<commit_before>'use strict';
var assert = require('chai').assert;
var fs = require('fs');
var path = require('path');
var Mode = require('stat-mode');
var rm = require('rimraf').sync;
var equal = require('assert-dir-equal');
var exec = require('child_process').exec;
var fixture = path.resolve.bind(path, __dirname, 'fixtures');

var Librarian = require('../');

describe('Librarian', function(){

    var NOOP = function(){};

    it('should expose constructor', function(){
        assert.isFunction(Librarian);
    });

    it('should not require new keyword', function(){
        var l = Librarian();
        assert.instanceOf(l, Librarian);
    });

    it('should have a DEFAULTS object', function(){
        assert.isObject(Librarian.DEFAULTS);
    });

    it('should take an options object', function(){
        var options = {directory:'my-directory'};
        var l = Librarian(options);
        assert.ok(l.directory(), 'my-directory');
    });

    it('should take a single string argument that will be the working directory', function(){
        //this might change!
        var l = Librarian('my-directory');
        assert.ok(l.directory(), 'my-directory');
    });

    describe('∆ use', function(){
        it('should add plugin to stack', function(){
            var l = Librarian();
            l.use(NOOP);
            assert.equal(l._plugins.length, 1);
        });
    });

    describe('∆ directory', function(){

        it('should set a working directory by default equal to process.env.PWD', function(){
            var l = Librarian();
            assert.equal(l.directory(), process.env.PWD);
        });

        it('should set the working directory', function(){
            var l = Librarian({directory:'dir'});
            assert.ok(l.directory(), 'dir');
        });

        it('should respect absolute directories', function(){
            var l = Librarian({directory:'/dir'});
            assert.ok(l.directory(), '/dir');
        });
    });

    describe('∆ source', function(){
        it('should set a "source" directory in DEFAULTS', function(){
            var l = Librarian();
            assert.ok(l.source(), Librarian.DEFAULTS.source);
        });

        it('should set the "source" directory', function(){
            var l = Librarian({source:'dir'});
            assert.ok(l.directory(), 'dir');
        });

        it('should get full path of source directory', function(){
            var l = Librarian();
            var absoluteBuildPath = path.join(process.env.PWD, '/build');
            assert.ok(l.source(), absoluteBuildPath);
        });

        it('should respect absolute directories', function(){
            var l = Librarian({source:'/dir'});
            assert.ok(l.source(), '/dir');
        });

        it('should throw on non strings', function(){
            var l = Librarian();
            assert.throws(function(){
                l.source(23);
            });
        });
    });

    describe('∆ destination', function(){
        it('should set a destination directory in DEFAULTS', function(){
            var l = Librarian();
            assert.ok(l.destination(), Librarian.DEFAULTS.destination);
        });

        it('should get full path of destination directory', function(){
            var l = Librarian();
            var absoluteBuildPath = path.join(process.env.PWD, '/build');
            assert.ok(l.destination(), absoluteBuildPath);
        });

        it('should respect absolute directories', function(){
            var l = Librarian({destination:'/dir'});
            assert.ok(l.destination(), '/dir');
        });

        it('should throw on non strings', function(){
            var l = Librarian();
            assert.throws(function(){
                l.destination(23);
            });
        });
    });

    describe('∆ clean', function(){
        it('should set the "clean" default used in DEFAULTS', function(){
            var l = Librarian();
            assert.equal(l._clean, Librarian.DEFAULTS.clean);
        });

        it('should set the "clean" option', function(){
            var l = Librarian();
            l.clean(false);
            assert.equal(l.clean(), false);
        });

        it('should throw on on boolean', function(){
            var l = Librarian();
            assert.throws(function(){
                l.clean(23);
            });
        });
    });

    describe('∆ debug', function(){
        it('should set the "debug" default used in DEFAULTS', function(){
            var l = Librarian();
            assert.equal(l._debug, Librarian.DEFAULTS.debug);
        });

        it('should set the "clean" option', function(){
            var l = Librarian();
            l.debug(false);
            assert.equal(l.debug(), false);
        });

        it('should throw on on boolean', function(){
            var l = Librarian();
            assert.throws(function(){
                l.debug(23);
            });
        });
    });

    describe('∆ frontmatter', function(){
        it('should set the "frontmatter" default used in DEFAULTS', function(){
            var l = Librarian();
            assert.equal(l.frontmatter(), Librarian.DEFAULTS.frontmatter);
        });

        it('should initialize the "frontmatter" option', function(){
            var l = Librarian({frontmatter: true});
            assert.equal(l.frontmatter(), true)
        });

        it('should set the "frontmatter"', function(){
            var l = Librarian();
            l.frontmatter(true);
            assert.equal(l.frontmatter(), true)
        });

        it('should throw on on boolean', function(){
            var l = Librarian();
            assert.throws(function(){
                l.frontmatter(23);
            });
        });
    });

    describe('∆ metadata', function(){
        it('should set the "metadata" default used in DEFAULTS', function(){
            var l = Librarian();
            assert.deepEqual(l.metadata(), Librarian.DEFAULTS.metadata);
        });

        it('should initialize the "metadata" option', function(){
            var meta = {
                user: 'goliatone',
                url:'http://goliatone.com'
            };

            var l = Librarian({metadata: meta});
            assert.deepEqual(l.metadata(), meta);
        });

        it('should get "metadata"', function(){
            var l = Librarian();
            assert.deepEqual(l.metadata(), {});
        });

        it('should set "metadata" clone', function(){
            var meta = {
                user: 'goliatone',
                url:'http://goliatone.com'
            };
            var l = Librarian();
            l.metadata(meta);
            assert.notEqual(l.metadata(), meta);
            assert.deepEqual(l.metadata(), meta);
        });
    });

    describe('∆ attributes', function(){
        it('should set the "attributes" default used in DEFAULTS', function(){
            var l = Librarian();
            assert.deepEqual(l._attributes, Librarian.DEFAULTS.attributes);
        });

        it('should initialize the "attributes" option', function(){
            var attributes = ['stats', 'content'];
            var l = Librarian({attributes: attributes});
            assert.ok(l.attributes(), attributes);
        });

        it('should get "attributes" option', function(){
            var l = Librarian();
            assert.deepEqual(l.attributes(), Librarian.DEFAULTS.attributes);
        });

        it('should set "attributes" option', function(){
            var l = Librarian();
            var attributes = ['stats', 'content'];
            l.attributes(attributes);
            assert.ok(l.attributes(), attributes);
        });
    });

    describe('∆ path', function(){
        it('should return a path relative to the working directory', function(){
            var l = Librarian({source:'test/tmp'});
            var rel = l.path('one', 'two', 'three');
            assert.ok(rel, '/test/tmp/one/two/three');
        });
    });

    describe('∆ read', function(){
        it('should read from a source directory', function(done){
            var l = Librarian({
                directory: fixture('read')
            });
            var stats = fs.statSync(fixture('read/src/index.md'));

            l.read(function(err, files){
                if(err) return done(err);
                assert.ok(files, {
                    'index.md': {
                        title: 'A Title',
                        contents: new Buffer('body'),
                        mode: stats.mode.toString(8).slice(-4),
                        stats:stats,
                        isUtf8: true
                    }
                });
                done();
            });
        });

        it('should read from a provided directory', function(done){
            var l = Librarian({
                directory: fixture('read-dir')
            });
            var stats = fs.statSync(fixture('read-dir/dir/index.md'));
            var dir = fixture('read-dir/dir');
            l.read(dir, function(err, files){
                if(err) done(err);
                assert.ok(files, {
                    'index.md': {
                        title: 'A Title',
                        contents: new Buffer('body'),
                        mode: stats.mode.toString(8).slice(-4),
                        stats:stats,
                        isUtf8: true
                    }
                });
            });
            done();
        });

        it('should preserve an existing file mode', function(done){
            var l = Librarian({
                directory: fixture('read-mode')
            });
            var stats = fs.statSync(fixture('read-mode/src/bin'));

            l.read(function(err, files){
                if(err) done(err);
                assert.ok(files, {
                    'bin': {
                        contents: new Buffer('echo test'),
                        mode: stats.mode.toString(8).slice(-4),
                        stats: stats,
                        isUtf8: false
                    }
                });
            });
            done();
        });

        describe('should expose isUtf8 "metadata" property', function(){
            it('should be false for binary files', function(done){
                var l = Librarian({
                    directory: fixture('read-isUtf8-false')
                });

                l.read(function(err, files){
                    if(err) done(err);
                    assert.isFalse(files['image.png'].isUtf8);
                });
                done();
            });

            it('should be true for text files', function(done){
                var l = Librarian({
                    directory: fixture('read-isUtf8-true')
                });

                l.read(function(err, files){
                    if(err) done(err);
                    assert.isTrue(files['index.md'].isUtf8);
                });
                done();
            });
        });

        it('should expose attributes in each file "metadata"', function(done){
            var l = Librarian({
                directory: fixture('expose-attributes')
            });
            l.read(function(err, files){
                if(err) done(err);
                var file = files['index.md'];
                Librarian.DEFAULTS.attributes.map(function(attr){
                    assert.property(file, attr, attr);
                });
            });
            done();
        });

        it('should not parse front-matter if "frontmatter" is false', function(done){
            var l = Librarian({
                directory: fixture('read-frontmatter'),
                frontmatter: false
            });
            l.read(function(err, files){
                if(err) done(err);
                assert.isUndefined(files['index.md'].title);
            });
            done();
        });

        it('should throw if provided "source" does not exist', function(done){
            var l = Librarian({
                directory: 'this-should-never-ever-ever-exist'
            });
            assert.throws(function(){
                l.read(function(err, files){
                    if(err) done(err);
                });
            });
        });
    });

    describe('∆ write', function(){
        it('should write to a destination directory', function(done){
            var l = Librarian({
                directory: fixture('write')
            });
            var files = {'index.md':{contents: new Buffer('body')}};
            l.write(files, function(err){
                if(err) return done(err);
                equal(fixture('write/build'), fixture('write/expected'));
                done();
            });
        });

        it('should write to a provided directory', function(done){
            var l = Librarian({
                directory: fixture('write-dir')
            });
            var files = {'index.md':{contents: new Buffer('body')}};
            var dir = fixture('write-dir/out');
            l.write(files, dir, function(err){
                if(err) return done(err);
                equal(fixture('write-dir/out'), fixture('write-dir/expected'));
                done();
            });
        });

        it('should chmod an optional mode from file metadata', function(done){
            var l = Librarian({
                directory: fixture('write-mode')
            });
            var files = {
                'bin':{
                    contents: new Buffer('echo test'),
                    mode: '0777'
                }
            };

            l.write(files, function(err){
                if(err) return done(err);
                var stats = fs.statSync(fixture('write-mode/build/bin'));
                var mode = Mode(stats).toOctal();
                assert.equal(mode, '0777');
                done();
            });
        });
    });

    describe('∆ run', function(){
        it('should apply a plugin', function(done){
            var l = Librarian();
            l.use(plugin);
            l.run({one: 'one'}, function(err, files, lbr){
                assert.property(files, 'one');
                assert.property(files, 'two');
                done();
            });

            function plugin(files, lbr, next){
                assert.property(files, 'one');
                assert.equal(l, lbr);
                assert.isFunction(next);
                files.two = 'two';
                next();
            }
        });

        it('should run with a provided plugin', function(done){
            var l = Librarian();
            //TODO: Review, how does it work? run takes two args?!
            l.run({one:'one'}, [plugin], function(err, files, lbr){
                assert.property(files, 'one');
                assert.property(files, 'two');
                done();
            });

            function plugin(files, lbr, next){
                assert.property(files, 'one');
                assert.equal(l, lbr);
                assert.isFunction(next);
                files.two = 'two';
                next();
            }
        });

        it('should support synchronous plugins', function(done){
            var l = Librarian();
            l.use(plugin);
            l.run({one: 'one'}, function(err, files, lbr){
                assert.property(files, 'one');
                assert.property(files, 'two');
                done();
            });

            function plugin(files, lbr){
                assert.property(files, 'one');
                assert.equal(l, lbr);
                files.two = 'two';
            }
        });
    });

    describe('∆ build', function(){
        it('should do a basic copy with no plugins', function(done){
            Librarian({
                directory: fixture('basic')
                // ,debug:true
            }).build(function(err, files){
                if(err) return done(err);
                assert.isObject(files);
                equal(fixture('basic/build'), fixture('basic/expected'));
                done();
            });
        });
        it('should preserve binary files', function(done){
            Librarian({
                directory: fixture('basic-images')
            }).build(function(err, files){
                if(err) return done(err);
                assert.isObject(files);
                equal(fixture('basic-images/build'), fixture('basic-images/expected'));
                done();
            });
        });

        it('should apply a plugin', function(done){
            Librarian({
                directory: fixture('basic-plugin')
            }).use(function(files, lbr, next){
                Object.keys(files).forEach(function(file){
                    var data = files[file];
                    data.contents = new Buffer(data.title);
                });
                next();
            })
            .build(function(err, files){
                if(err) return done(err);
                assert.isObject(files);
                equal(fixture('basic-plugin/build'), fixture('basic-plugin/expected'));
                done();
            });
        });

        it('should remove an existing build directory', function(done){
            var l = Librarian({
                directory: fixture('build')
            });

            rm(fixture('build/build'));
            fs.mkdirSync(fixture('build/build'));

            exec('touch tests/fixtures/build/build/empty.md', function(err){
                if(err) return done(err);
                l.build(function(err){
                    if(err) return done(err);
                    equal(fixture('build/build'), fixture('build/expected'));
                    done();
                });
            });
        });

        it('should not remove an existing build directory if "clean" is false', function(done){
            var l = Librarian({
                directory: fixture('build-noclean'),
                clean: false
            });

            exec('mkdir -p tests/fixtures/build-noclean/build && \
             touch tests/fixtures/build-noclean/build/empty.md', function(err){
                if(err) return done(err);
                var files = {'index.md': {contents: new Buffer('body')}};
                l.build(function(err){
                    if(err) return done(err);
                    equal(fixture('build-noclean/build'), fixture('build-noclean/expected'));
                    done();
                });
            });
        });
    });

    describe('∆ debugBuild', function(){
        xit('should have configurable output path', function(){});
        xit('should generate metadata file', function(done){});
    });
});<commit_msg>TESTS: librarian wip<commit_after>'use strict';
var assert = require('chai').assert;
var fs = require('fs');
var path = require('path');
var Mode = require('stat-mode');
var rm = require('rimraf').sync;
var equal = require('assert-dir-equal');
var exec = require('child_process').exec;
var fixture = path.resolve.bind(path, __dirname, 'fixtures');

var Librarian = require('../');

describe('Librarian', function(){

    var NOOP = function(){};

    it('should expose constructor', function(){
        assert.isFunction(Librarian);
    });

    it('should not require new keyword', function(){
        var l = Librarian();
        assert.instanceOf(l, Librarian);
    });

    it('should have a DEFAULTS object', function(){
        assert.isObject(Librarian.DEFAULTS);
    });

    it('should take an options object', function(){
        var options = {directory:'my-directory'};
        var l = Librarian(options);
        assert.ok(l.directory(), 'my-directory');
    });

    it('should take a single string argument that will be the working directory', function(){
        //this might change!
        var l = Librarian('my-directory');
        assert.ok(l.directory(), 'my-directory');
    });

    describe('∆ use', function(){
        it('should add plugin to stack', function(){
            var l = Librarian();
            l.use(NOOP);
            assert.equal(l._plugins.length, 1);
        });
    });

    describe('∆ directory', function(){

        it('should set a working directory by default equal to process.env.PWD', function(){
            var l = Librarian();
            assert.equal(l.directory(), process.env.PWD);
        });

        it('should set the working directory', function(){
            var l = Librarian({directory:'dir'});
            assert.ok(l.directory(), 'dir');
        });

        it('should respect absolute directories', function(){
            var l = Librarian({directory:'/dir'});
            assert.ok(l.directory(), '/dir');
        });
    });

    describe('∆ source', function(){
        it('should set a "source" directory in DEFAULTS', function(){
            var l = Librarian();
            assert.ok(l.source(), Librarian.DEFAULTS.source);
        });

        it('should set the "source" directory', function(){
            var l = Librarian({source:'dir'});
            assert.ok(l.directory(), 'dir');
        });

        it('should get full path of source directory', function(){
            var l = Librarian();
            var absoluteBuildPath = path.join(process.env.PWD, '/build');
            assert.ok(l.source(), absoluteBuildPath);
        });

        it('should respect absolute directories', function(){
            var l = Librarian({source:'/dir'});
            assert.ok(l.source(), '/dir');
        });

        it('should throw on non strings', function(){
            var l = Librarian();
            assert.throws(function(){
                l.source(23);
            });
        });
    });

    describe('∆ destination', function(){
        it('should set a destination directory in DEFAULTS', function(){
            var l = Librarian();
            assert.ok(l.destination(), Librarian.DEFAULTS.destination);
        });

        it('should get full path of destination directory', function(){
            var l = Librarian();
            var absoluteBuildPath = path.join(process.env.PWD, '/build');
            assert.ok(l.destination(), absoluteBuildPath);
        });

        it('should respect absolute directories', function(){
            var l = Librarian({destination:'/dir'});
            assert.ok(l.destination(), '/dir');
        });

        it('should throw on non strings', function(){
            var l = Librarian();
            assert.throws(function(){
                l.destination(23);
            });
        });
    });

    describe('∆ clean', function(){
        it('should set the "clean" default used in DEFAULTS', function(){
            var l = Librarian();
            assert.equal(l._clean, Librarian.DEFAULTS.clean);
        });

        it('should set the "clean" option', function(){
            var l = Librarian();
            l.clean(false);
            assert.equal(l.clean(), false);
        });

        it('should throw on on boolean', function(){
            var l = Librarian();
            assert.throws(function(){
                l.clean(23);
            });
        });
    });

    describe('∆ debug', function(){
        it('should set the "debug" default used in DEFAULTS', function(){
            var l = Librarian();
            assert.equal(l._debug, Librarian.DEFAULTS.debug);
        });

        it('should set the "clean" option', function(){
            var l = Librarian();
            l.debug(false);
            assert.equal(l.debug(), false);
        });

        it('should throw on on boolean', function(){
            var l = Librarian();
            assert.throws(function(){
                l.debug(23);
            });
        });
    });

    describe('∆ frontmatter', function(){
        it('should set the "frontmatter" default used in DEFAULTS', function(){
            var l = Librarian();
            assert.equal(l.frontmatter(), Librarian.DEFAULTS.frontmatter);
        });

        it('should initialize the "frontmatter" option', function(){
            var l = Librarian({frontmatter: true});
            assert.equal(l.frontmatter(), true)
        });

        it('should set the "frontmatter"', function(){
            var l = Librarian();
            l.frontmatter(true);
            assert.equal(l.frontmatter(), true)
        });

        it('should throw on on boolean', function(){
            var l = Librarian();
            assert.throws(function(){
                l.frontmatter(23);
            });
        });
    });

    describe('∆ metadata', function(){
        it('should set the "metadata" default used in DEFAULTS', function(){
            var l = Librarian();
            assert.deepEqual(l.metadata(), Librarian.DEFAULTS.metadata);
        });

        it('should initialize the "metadata" option', function(){
            var meta = {
                user: 'goliatone',
                url:'http://goliatone.com'
            };

            var l = Librarian({metadata: meta});
            assert.deepEqual(l.metadata(), meta);
        });

        it('should get "metadata"', function(){
            var l = Librarian();
            assert.deepEqual(l.metadata(), {});
        });

        it('should set "metadata" clone', function(){
            var meta = {
                user: 'goliatone',
                url:'http://goliatone.com'
            };
            var l = Librarian();
            l.metadata(meta);
            assert.notEqual(l.metadata(), meta);
            assert.deepEqual(l.metadata(), meta);
        });
    });

    describe('∆ attributes', function(){
        it('should set the "attributes" default used in DEFAULTS', function(){
            var l = Librarian();
            assert.deepEqual(l._attributes, Librarian.DEFAULTS.attributes);
        });

        it('should initialize the "attributes" option', function(){
            var attributes = ['stats', 'content'];
            var l = Librarian({attributes: attributes});
            assert.ok(l.attributes(), attributes);
        });

        it('should get "attributes" option', function(){
            var l = Librarian();
            assert.deepEqual(l.attributes(), Librarian.DEFAULTS.attributes);
        });

        it('should set "attributes" option', function(){
            var l = Librarian();
            var attributes = ['stats', 'content'];
            l.attributes(attributes);
            assert.ok(l.attributes(), attributes);
        });
    });

    describe('∆ path', function(){
        it('should return a path relative to the working directory', function(){
            var l = Librarian({source:'test/tmp'});
            var rel = l.path('one', 'two', 'three');
            assert.ok(rel, '/test/tmp/one/two/three');
        });
    });

    describe('∆ read', function(){
        it('should read from a source directory', function(done){
            var l = Librarian({
                directory: fixture('read')
            });
            var stats = fs.statSync(fixture('read/src/index.md'));

            l.read(function(err, files){
                if(err) return done(err);
                assert.ok(files, {
                    'index.md': {
                        title: 'A Title',
                        contents: new Buffer('body'),
                        mode: stats.mode.toString(8).slice(-4),
                        stats:stats,
                        isUtf8: true
                    }
                });
                done();
            });
        });

        it('should read from a provided directory', function(done){
            var l = Librarian({
                directory: fixture('read-dir')
            });
            var stats = fs.statSync(fixture('read-dir/dir/index.md'));
            var dir = fixture('read-dir/dir');
            l.read(dir, function(err, files){
                if(err) done(err);
                assert.ok(files, {
                    'index.md': {
                        title: 'A Title',
                        contents: new Buffer('body'),
                        mode: stats.mode.toString(8).slice(-4),
                        stats:stats,
                        isUtf8: true
                    }
                });
            });
            done();
        });

        it('should preserve an existing file mode', function(done){
            var l = Librarian({
                directory: fixture('read-mode')
            });
            var stats = fs.statSync(fixture('read-mode/src/bin'));

            l.read(function(err, files){
                if(err) done(err);
                assert.ok(files, {
                    'bin': {
                        contents: new Buffer('echo test'),
                        mode: stats.mode.toString(8).slice(-4),
                        stats: stats,
                        isUtf8: false
                    }
                });
            });
            done();
        });

        describe('should expose isUtf8 "metadata" property', function(){
            it('should be false for binary files', function(done){
                var l = Librarian({
                    directory: fixture('read-isUtf8-false')
                });

                l.read(function(err, files){
                    if(err) done(err);
                    assert.isFalse(files['image.png'].isUtf8);
                });
                done();
            });

            it('should be true for text files', function(done){
                var l = Librarian({
                    directory: fixture('read-isUtf8-true')
                });

                l.read(function(err, files){
                    if(err) done(err);
                    assert.isTrue(files['index.md'].isUtf8);
                });
                done();
            });
        });

        it('should expose attributes in each file "metadata"', function(done){
            var l = Librarian({
                directory: fixture('expose-attributes')
            });
            l.read(function(err, files){
                if(err) done(err);
                var file = files['index.md'];
                Librarian.DEFAULTS.attributes.map(function(attr){
                    assert.property(file, attr, attr);
                });
            });
            done();
        });

        it('should not parse front-matter if "frontmatter" is false', function(done){
            var l = Librarian({
                directory: fixture('read-frontmatter'),
                frontmatter: false
            });
            l.read(function(err, files){
                if(err) done(err);
                assert.isUndefined(files['index.md'].title);
            });
            done();
        });

        it('should throw if provided "source" does not exist', function(done){
            var l = Librarian({
                directory: 'this-should-never-ever-ever-exist'
            });
            l.read(function(err){
                assert(err);
                done();
            });
        });
    });

    describe('∆ write', function(){
        it('should write to a destination directory', function(done){
            var l = Librarian({
                directory: fixture('write')
            });
            var files = {'index.md':{contents: new Buffer('body')}};
            l.write(files, function(err){
                if(err) return done(err);
                equal(fixture('write/build'), fixture('write/expected'));
                done();
            });
        });

        it('should write to a provided directory', function(done){
            var l = Librarian({
                directory: fixture('write-dir')
            });
            var files = {'index.md':{contents: new Buffer('body')}};
            var dir = fixture('write-dir/out');
            l.write(files, dir, function(err){
                if(err) return done(err);
                equal(fixture('write-dir/out'), fixture('write-dir/expected'));
                done();
            });
        });

        it('should chmod an optional mode from file metadata', function(done){
            var l = Librarian({
                directory: fixture('write-mode')
            });
            var files = {
                'bin':{
                    contents: new Buffer('echo test'),
                    mode: '0777'
                }
            };

            l.write(files, function(err){
                if(err) return done(err);
                var stats = fs.statSync(fixture('write-mode/build/bin'));
                var mode = Mode(stats).toOctal();
                assert.equal(mode, '0777');
                done();
            });
        });
    });

    describe('∆ run', function(){
        it('should apply a plugin', function(done){
            var l = Librarian();
            l.use(plugin);
            l.run({one: 'one'}, function(err, files, lbr){
                assert.property(files, 'one');
                assert.property(files, 'two');
                done();
            });

            function plugin(files, lbr, next){
                assert.property(files, 'one');
                assert.equal(l, lbr);
                assert.isFunction(next);
                files.two = 'two';
                next();
            }
        });

        it('should run with a provided plugin', function(done){
            var l = Librarian();
            //TODO: Review, how does it work? run takes two args?!
            l.run({one:'one'}, [plugin], function(err, files, lbr){
                assert.property(files, 'one');
                assert.property(files, 'two');
                done();
            });

            function plugin(files, lbr, next){
                assert.property(files, 'one');
                assert.equal(l, lbr);
                assert.isFunction(next);
                files.two = 'two';
                next();
            }
        });

        it('should support synchronous plugins', function(done){
            var l = Librarian();
            l.use(plugin);
            l.run({one: 'one'}, function(err, files, lbr){
                assert.property(files, 'one');
                assert.property(files, 'two');
                done();
            });

            function plugin(files, lbr){
                assert.property(files, 'one');
                assert.equal(l, lbr);
                files.two = 'two';
            }
        });
    });

    describe('∆ build', function(){
        it('should do a basic copy with no plugins', function(done){
            Librarian({
                directory: fixture('basic')
                // ,debug:true
            }).build(function(err, files){
                if(err) return done(err);
                assert.isObject(files);
                equal(fixture('basic/build'), fixture('basic/expected'));
                done();
            });
        });
        it('should preserve binary files', function(done){
            Librarian({
                directory: fixture('basic-images')
            }).build(function(err, files){
                if(err) return done(err);
                assert.isObject(files);
                equal(fixture('basic-images/build'), fixture('basic-images/expected'));
                done();
            });
        });

        it('should apply a plugin', function(done){
            Librarian({
                directory: fixture('basic-plugin')
            }).use(function(files, lbr, next){
                Object.keys(files).forEach(function(file){
                    var data = files[file];
                    data.contents = new Buffer(data.title);
                });
                next();
            })
            .build(function(err, files){
                if(err) return done(err);
                assert.isObject(files);
                equal(fixture('basic-plugin/build'), fixture('basic-plugin/expected'));
                done();
            });
        });

        it('should remove an existing build directory', function(done){
            var l = Librarian({
                directory: fixture('build')
            });

            rm(fixture('build/build'));
            fs.mkdirSync(fixture('build/build'));

            exec('touch tests/fixtures/build/build/empty.md', function(err){
                if(err) return done(err);
                l.build(function(err){
                    if(err) return done(err);
                    equal(fixture('build/build'), fixture('build/expected'));
                    done();
                });
            });
        });

        it('should not remove an existing build directory if "clean" is false', function(done){
            var l = Librarian({
                directory: fixture('build-noclean'),
                clean: false
            });

            exec('mkdir -p tests/fixtures/build-noclean/build && \
             touch tests/fixtures/build-noclean/build/empty.md', function(err){
                if(err) return done(err);
                var files = {'index.md': {contents: new Buffer('body')}};
                l.build(function(err){
                    if(err) return done(err);
                    equal(fixture('build-noclean/build'), fixture('build-noclean/expected'));
                    done();
                });
            });
        });
    });

    describe('∆ debugBuild', function(){
        xit('should have configurable output path', function(){});
        xit('should generate metadata file', function(done){});
    });
});<|endoftext|>


<commit_before>import React from 'react';

export function createStyleObject(classNames, elementStyle = {}, stylesheet) {
  return classNames.reduce((styleObject, className) => {
    return {...styleObject, ...stylesheet[className]};
  }, elementStyle);
}

export function createClassNameString(classNames) {
  return classNames.join(' ');
}

export function createChildren(stylesheet, useInlineStyles) {
  let childrenCount = 0;
  return children => {
    childrenCount += 1;
    return children.map((child, i) => createElement({
      node: child,
      stylesheet,
      useInlineStyles,
      key:`code-segment-${childrenCount}-${i}`
    }));
  }
}

export default function createElement({ node, stylesheet, style = {}, useInlineStyles, key }) {
  const { properties, type, tagName: TagName, value } = node;
  if (type === 'text') {
    return value;
  } else if (TagName) {
    const childrenCreator = createChildren(stylesheet, useInlineStyles);
    const props = (
      useInlineStyles
      ?
      {
        ...properties,
        ...{ className: undefined },
        style: createStyleObject(
          properties.className,
          Object.assign({}, properties.style, style),
          stylesheet
        ),
      }
      :
      { ...properties, className: createClassNameString(properties.className) }
    );
    const children = childrenCreator(node.children);
    return <TagName key={key} {...props}>{children}</TagName>;
  }
}<commit_msg>pass along classNames that are not part of stylesheet even when using inline styles<commit_after>import React from 'react';

export function createStyleObject(classNames, elementStyle = {}, stylesheet) {
  return classNames.reduce((styleObject, className) => {
    return {...styleObject, ...stylesheet[className]};
  }, elementStyle);
}

export function createClassNameString(classNames) {
  return classNames.join(' ');
}

export function createChildren(stylesheet, useInlineStyles) {
  let childrenCount = 0;
  return children => {
    childrenCount += 1;
    return children.map((child, i) => createElement({
      node: child,
      stylesheet,
      useInlineStyles,
      key:`code-segment-${childrenCount}-${i}`
    }));
  }
}

export default function createElement({ node, stylesheet, style = {}, useInlineStyles, key }) {
  const { properties, type, tagName: TagName, value } = node;
  if (type === 'text') {
    return value;
  } else if (TagName) {
    const childrenCreator = createChildren(stylesheet, useInlineStyles);
    const nonStylesheetClassNames = (
      useInlineStyles &&
      properties.className &&
      properties.className.filter(className => !stylesheet[className])
    );
    const className = (
      nonStylesheetClassNames && nonStylesheetClassNames.length
      ?
      nonStylesheetClassNames
      :
      undefined
    )
    const props = (
      useInlineStyles
      ?
      {
        ...properties,
        ...{ className },
        style: createStyleObject(
          properties.className,
          Object.assign({}, properties.style, style),
          stylesheet
        ),
      }
      :
      { ...properties, className: createClassNameString(properties.className) }
    );
    const children = childrenCreator(node.children);
    return <TagName key={key} {...props}>{children}</TagName>;
  }
}<|endoftext|>


<commit_before>import splat from "./splat";
import formatDeclarations from "./formatDeclarations";

export default function createRenderer(backend) {
    let globalSpec = 0;
    const specs = Object.create(null);

    return function render(rules) {
        let currentSpec = -1;

        const className = splat(rules).map((rule) => {
            let ruleSpec = -1;
            const ruleSpecs = specs[rule.id] || (specs[rule.id] = []);

            for (let i = 0; i < ruleSpecs.length; i++) {
                if (ruleSpecs[i] > currentSpec) {
                    ruleSpec = ruleSpecs[i];
                    break;
                }
            }

            if (ruleSpec < 0) {
                formatDeclarations(`.${rule.class}__${globalSpec}`,
                                    rule.declarations,
                                    backend);
                ruleSpecs.push(globalSpec);
                ruleSpec = globalSpec;
                globalSpec += 1;
            }

            currentSpec = ruleSpec;

            return `${rule.class}__${ruleSpec}`;
        }).join(" ");

        return {
            toString () {
                return className;
            },
            _rules: rules,
        };
    };
}

<commit_msg>support for preact 7<commit_after>import splat from "./splat";
import formatDeclarations from "./formatDeclarations";

export default function createRenderer(backend) {
    let globalSpec = 0;
    const specs = Object.create(null);

    return function render(rules) {
        let currentSpec = -1;

        const classNames = splat(rules).map((rule) => {
            let ruleSpec = -1;
            const ruleSpecs = specs[rule.id] || (specs[rule.id] = []);

            for (let i = 0; i < ruleSpecs.length; i++) {
                if (ruleSpecs[i] > currentSpec) {
                    ruleSpec = ruleSpecs[i];
                    break;
                }
            }

            if (ruleSpec < 0) {
                formatDeclarations(`.${rule.class}__${globalSpec}`,
                                    rule.declarations,
                                    backend);
                ruleSpecs.push(globalSpec);
                ruleSpec = globalSpec;
                globalSpec += 1;
            }

            currentSpec = ruleSpec;

            return `${rule.class}__${ruleSpec}`;
        });

        const renderer = {};
        Object.defineProperties(renderer, {
            toString: {
                value() { return classNames.join(" "); },
            },
            _rules: { value: rules },
        });
        classNames.forEach((className) => renderer[className] = true);

        return renderer;
    };
}

<|endoftext|>


<commit_before>import momentTimezone from 'moment-timezone';

import hasOverlap from './hasOverlap';

function compareDates(a, b) {
  return a.start < b.start ? -1 : 1;
}

function groupEvents(events) {
  const groups = [];
  events.forEach((event) => {
    // Add all allDay events in separate groups
    if (!event.allDay) {
      return;
    }
    groups.push({
      start: event.start,
      end: event.end,
      columns: [[event]],
    });
  });

  let currentGroup;
  events.forEach((event) => {
    if (event.allDay) {
      // ignore
      return;
    }
    if (currentGroup && event.start >= currentGroup.end) {
      currentGroup = undefined;
    }
    if (currentGroup) {
      const existingCol = currentGroup.columns.find(
        column => column.some(({ end }) => end <= event.start));
      if (existingCol) {
        existingCol.push(event);
      } else {
        currentGroup.columns.push([event]);
      }
    }
    if (!currentGroup) {
      currentGroup = {
        start: event.start,
        end: event.end,
        columns: [[event]],
      };
      groups.push(currentGroup);
    }
    currentGroup.end = Math.max(currentGroup.end, event.end);
  });
  return groups;
}

function flattenGroups(groups) {
  const result = [];
  groups.forEach((group) => {
    const columnsLength = group.columns.length;
    group.columns.forEach((events, columnIndex) => {
      events.forEach((event) => {
        let colspan = 1;
        // Peek ahead to see if the event fits in another column
        let j = columnIndex + 1;
        while (
          j < group.columns.length &&
          !hasOverlap(group.columns[j], event.start, event.end)
        ) {
          colspan++;
          j++;
        }
        result.push(Object.assign({
          width: colspan / columnsLength,
          offset: columnIndex / columnsLength,
        }, event));
      });
    });
  });
  result.sort(compareDates);
  return result;
}

function normalize(events, timeZone) {
  return events.map(event => Object.assign({}, event, {
    start: momentTimezone.tz(event.start, timeZone).toDate(),
    end: momentTimezone.tz(event.end, timeZone).toDate(),
  }));
}

export default function decorateEvents(events, timeZone) {
  // Make sure events are sorted by start time
  const orderedByStartTime = normalize(events).sort(compareDates);
  const groups = groupEvents(orderedByStartTime);
  return flattenGroups(groups);
}
<commit_msg>Fix using unused variable timeZone<commit_after>import momentTimezone from 'moment-timezone';

import hasOverlap from './hasOverlap';

function compareDates(a, b) {
  return a.start < b.start ? -1 : 1;
}

function groupEvents(events) {
  const groups = [];
  events.forEach((event) => {
    // Add all allDay events in separate groups
    if (!event.allDay) {
      return;
    }
    groups.push({
      start: event.start,
      end: event.end,
      columns: [[event]],
    });
  });

  let currentGroup;
  events.forEach((event) => {
    if (event.allDay) {
      // ignore
      return;
    }
    if (currentGroup && event.start >= currentGroup.end) {
      currentGroup = undefined;
    }
    if (currentGroup) {
      const existingCol = currentGroup.columns.find(
        column => column.some(({ end }) => end <= event.start));
      if (existingCol) {
        existingCol.push(event);
      } else {
        currentGroup.columns.push([event]);
      }
    }
    if (!currentGroup) {
      currentGroup = {
        start: event.start,
        end: event.end,
        columns: [[event]],
      };
      groups.push(currentGroup);
    }
    currentGroup.end = Math.max(currentGroup.end, event.end);
  });
  return groups;
}

function flattenGroups(groups) {
  const result = [];
  groups.forEach((group) => {
    const columnsLength = group.columns.length;
    group.columns.forEach((events, columnIndex) => {
      events.forEach((event) => {
        let colspan = 1;
        // Peek ahead to see if the event fits in another column
        let j = columnIndex + 1;
        while (
          j < group.columns.length &&
          !hasOverlap(group.columns[j], event.start, event.end)
        ) {
          colspan++;
          j++;
        }
        result.push(Object.assign({
          width: colspan / columnsLength,
          offset: columnIndex / columnsLength,
        }, event));
      });
    });
  });
  result.sort(compareDates);
  return result;
}

function normalize(events, timeZone) {
  return events.map(event => Object.assign({}, event, {
    start: momentTimezone.tz(event.start, timeZone).toDate(),
    end: momentTimezone.tz(event.end, timeZone).toDate(),
  }));
}

export default function decorateEvents(events, timeZone) {
  // Make sure events are sorted by start time
  const orderedByStartTime = normalize(events, timeZone).sort(compareDates);
  const groups = groupEvents(orderedByStartTime);
  return flattenGroups(groups);
}
<|endoftext|>


<commit_before>/**
 * Created by mgoria on 5/28/15.
 */

'use strict';

import S3FS from 's3fs';
import Kernel from 'deep-kernel';
import {UnknownFolderException} from './Exception/UnknownFolderException';
import OS from 'os';
import Path from 'path';
import {Registry} from './Registry';

/**
 * Deep FS implementation
 */
export class FS extends Kernel.ContainerAware {
  /**
   * Defines all class private properties
   *
   * @param {String} tmpFsBucket
   * @param {String} publicFsBucket
   * @param {String} systemFsBucket
   */
  constructor(tmpFsBucket = null, publicFsBucket = null, systemFsBucket = null) {
    super();

    this._mountedFolders = {};
    this._buckets = {};

    this._buckets[FS.TMP] = tmpFsBucket;
    this._buckets[FS.PUBLIC] = publicFsBucket;
    this._buckets[FS.SYSTEM] = systemFsBucket;

    this._registry = null;
  }

  /**
   * @returns {Registry}
   */
  get registry() {
    return this._registry;
  }

  /**
   * @returns {string}
   */
  static get TMP() {
    return 'temp';
  }

  /**
   * @returns {string}
   */
  static get PUBLIC() {
    return 'public';
  }

  /**
   * @returns {string}
   */
  static get SYSTEM() {
    return 'system';
  }

  /**
   * @returns {Array}
   */
  static get FOLDERS() {
    return [
      FS.TMP,
      FS.PUBLIC,
      FS.SYSTEM,
    ];
  }

  /**
   * Booting a certain service
   *
   * @param {Kernel} kernel
   * @param {Function} callback
   */
  boot(kernel, callback) {
    let bucketsConfig = kernel.config.buckets;

    for (let folderKey in FS.FOLDERS) {
      if (!FS.FOLDERS.hasOwnProperty(folderKey)) {
        continue;
      }

      let folder = FS.FOLDERS[folderKey];

      this._buckets[folder] = `${bucketsConfig[folder].name}/${kernel.microservice().identifier}`;
    }

    this._registry = Registry.createFromFS(this.public);

    callback();
  }

  /**
   * Returns mounted file system folder (tmp, public or system)
   *
   * @param name
   * @returns {*}
   */
  getFolder(name) {
    if (FS.FOLDERS.indexOf(name) === -1) {
      throw new UnknownFolderException(name, FS.FOLDERS);
    }

    if (typeof this._mountedFolders[name] === 'undefined') {
      if (this._localBackend) {
        let rootFolder = FS._getTmpDir(this._buckets[name]);
        let SimulatedS3FS = require('./Local/S3FSRelativeFSExtender').S3FSRelativeFSExtender;

        this._mountedFolders[name] = new SimulatedS3FS(rootFolder).relativeFsExtended;
      } else {
        let options = {
          params: {
            Bucket: this._buckets[name],
          },
        };

        this._mountedFolders[name] = new S3FS(this._buckets[name], options);
      }
    }

    return this._mountedFolders[name];
  }

  /**
   * @param {String} subpath
   * @returns {String}
   * @private
   */
  static _getTmpDir(subpath) {
    let dir = Path.join(OS.tmpdir(), subpath);

    require('fs-extra').mkdirpSync(dir);

    return dir;
  }

  /**
   * Returns mounted tmp folder
   *
   * @returns {*}
   */
  get tmp() {
    return this.getFolder(FS.TMP);
  }

  /**
   * Returns mounted public folder
   *
   * @returns {*}
   */
  get public() {
    return this.getFolder(FS.PUBLIC);
  }

  /**
   * Returns mounted sys folder
   *
   * @returns {*}
   */
  get system() {
    return this.getFolder(FS.SYSTEM);
  }
}
<commit_msg>Use system bucket<commit_after>/**
 * Created by mgoria on 5/28/15.
 */

'use strict';

import S3FS from 's3fs';
import Kernel from 'deep-kernel';
import {UnknownFolderException} from './Exception/UnknownFolderException';
import OS from 'os';
import Path from 'path';
import {Registry} from './Registry';

/**
 * Deep FS implementation
 */
export class FS extends Kernel.ContainerAware {
  /**
   * Defines all class private properties
   *
   * @param {String} tmpFsBucket
   * @param {String} publicFsBucket
   * @param {String} systemFsBucket
   */
  constructor(tmpFsBucket = null, publicFsBucket = null, systemFsBucket = null) {
    super();

    this._mountedFolders = {};
    this._buckets = {};

    this._buckets[FS.TMP] = tmpFsBucket;
    this._buckets[FS.PUBLIC] = publicFsBucket;
    this._buckets[FS.SYSTEM] = systemFsBucket;

    this._registry = null;
  }

  /**
   * @returns {Registry}
   */
  get registry() {
    return this._registry;
  }

  /**
   * @returns {string}
   */
  static get TMP() {
    return 'temp';
  }

  /**
   * @returns {string}
   */
  static get PUBLIC() {
    return 'public';
  }

  /**
   * @returns {string}
   */
  static get SYSTEM() {
    return 'system';
  }

  /**
   * @returns {Array}
   */
  static get FOLDERS() {
    return [
      FS.TMP,
      FS.PUBLIC,
      FS.SYSTEM,
    ];
  }

  /**
   * Booting a certain service
   *
   * @param {Kernel} kernel
   * @param {Function} callback
   */
  boot(kernel, callback) {
    let bucketsConfig = kernel.config.buckets;

    for (let folderKey in FS.FOLDERS) {
      if (!FS.FOLDERS.hasOwnProperty(folderKey)) {
        continue;
      }

      let folder = FS.FOLDERS[folderKey];

      this._buckets[folder] = `${bucketsConfig[folder].name}/${kernel.microservice().identifier}`;
    }

    this._registry = Registry.createFromFS(this.system);

    callback();
  }

  /**
   * Returns mounted file system folder (tmp, public or system)
   *
   * @param name
   * @returns {*}
   */
  getFolder(name) {
    if (FS.FOLDERS.indexOf(name) === -1) {
      throw new UnknownFolderException(name, FS.FOLDERS);
    }

    if (typeof this._mountedFolders[name] === 'undefined') {
      if (this._localBackend) {
        let rootFolder = FS._getTmpDir(this._buckets[name]);
        let SimulatedS3FS = require('./Local/S3FSRelativeFSExtender').S3FSRelativeFSExtender;

        this._mountedFolders[name] = new SimulatedS3FS(rootFolder).relativeFsExtended;
      } else {
        let options = {
          params: {
            Bucket: this._buckets[name],
          },
        };

        this._mountedFolders[name] = new S3FS(this._buckets[name], options);
      }
    }

    return this._mountedFolders[name];
  }

  /**
   * @param {String} subpath
   * @returns {String}
   * @private
   */
  static _getTmpDir(subpath) {
    let dir = Path.join(OS.tmpdir(), subpath);

    require('fs-extra').mkdirpSync(dir);

    return dir;
  }

  /**
   * Returns mounted tmp folder
   *
   * @returns {*}
   */
  get tmp() {
    return this.getFolder(FS.TMP);
  }

  /**
   * Returns mounted public folder
   *
   * @returns {*}
   */
  get public() {
    return this.getFolder(FS.PUBLIC);
  }

  /**
   * Returns mounted sys folder
   *
   * @returns {*}
   */
  get system() {
    return this.getFolder(FS.SYSTEM);
  }
}
<|endoftext|>


<commit_before>/**
*
* Main app file, this one is responsible of the load of any used modules so as to their initilisation
* against dom elements.
*
* Keep in mind that you can get a quick access to the internal stored object with container.data('modulename')
*
*/
(function($) {

    require(

    // Load in modules
    ['app/modules/wiki', 'app/modules/messaging', 'app/modules/history', 'app/modules/highlight'],

    function(wiki, messaging, history, highlight) {

        $(function() {
          var container = $('#container');

          container
              // Our main module
              .wikiConvertor({
                  wikiPath: container.data('wiki') || '',
                  main: '.wikiconvertor-content'
              })

              // Add messaging support, user feedback and so on
              .messaging()

              // Add history and back button support
              .history()

              // Allow the hightlight of code snippets using SyntaxHighligter
              .highlight();

        });
    });

})(this.jQuery);
<commit_msg>history module init prior to the main one<commit_after>/**
*
* Main app file, this one is responsible of the load of any used modules so as to their initilisation
* against dom elements.
*
* Keep in mind that you can get a quick access to the internal stored object with container.data('modulename')
*
*/
(function($) {

    require(

    // Load in modules
    ['app/modules/wiki', 'app/modules/messaging', 'app/modules/history', 'app/modules/highlight'],

    function(wiki, messaging, history, highlight) {

        $(function() {
          var container = $('#container');

          container
              // Needed prior main module initialization
              // This misconception leads to unexpected behaviour on Opera 11
              // (haschange binding needed prior hash rewrite)
              .history()

              // Our main module
              .wikiConvertor({
                  wikiPath: container.data('wiki') || '',
                  main: '.wikiconvertor-content'
              })

              // Add messaging support, user feedback and so on
              .messaging()

              // Allow the hightlight of code snippets using SyntaxHighligter
              .highlight();

        });
    });

})(this.jQuery);
<|endoftext|>


<commit_before>;(function(context, MM) {

    var easey_handlers = {};

    easey_handlers.TouchHandler = function() {
        var handler = {},
            map,
            panner,
            maxTapTime = 250,
            maxTapDistance = 30,
            maxDoubleTapDelay = 350,
            locations = {},
            taps = [],
            wasPinching = false,
            lastPinchCenter = null,
            p0 = new MM.Point(0, 0),
            p1 = new MM.Point(0, 0);

        function focusMap(e) {
            map.parent.focus();
        }

        function clearLocations() {
            for (var loc in locations) {
                if (locations.hasOwnProperty(loc)) {
                    delete locations[loc];
                }
            }
        }

        function updateTouches (e) {
            for (var i = 0; i < e.touches.length; i += 1) {
                var t = e.touches[i];
                if (t.identifier in locations) {
                    var l = locations[t.identifier];
                    l.x = t.clientX;
                    l.y = t.clientY;
                    l.scale = e.scale;
                } else {
                    locations[t.identifier] = {
                        scale: e.scale,
                        startPos: { x: t.clientX, y: t.screenY },
                        x: t.clientX,
                        y: t.clientY,
                        time: new Date().getTime()
                    };
                }
            }
        }

        function touchStartMachine(e) {
            updateTouches(e);
            panner.down(e.touches[0]);
            return MM.cancelEvent(e);
        }

        function touchMoveMachine(e) {
            switch (e.touches.length) {
                case 1:
                    panner.move(e.touches[0]);
                    break;
                case 2:
                    onPinching(e);
                    break;
            }
            updateTouches(e);
            return MM.cancelEvent(e);
        }

        // Handle a tap event - mainly watch for a doubleTap
        function onTap(tap) {
            if (taps.length &&
                (tap.time - taps[0].time) < maxDoubleTapDelay) {
                onDoubleTap(tap);
                taps = [];
                return;
            }
            taps = [tap];
        }

        // Handle a double tap by zooming in a single zoom level to a
        // round zoom.
        function onDoubleTap(tap) {
            // zoom in to a round number
            easey().map(map)
            .to(map.pointCoordinate(tap).zoomTo(map.getZoom() + 1))
            .path('about').run(200, function() {
                map.dispatchCallback('zoomed');
                clearLocations();
            });
        }

        function isTouchable() {
            var el = document.createElement('div');
            el.setAttribute('ongesturestart', 'return;');
            return (typeof el.ongesturestart === 'function');
        }

        function onPinching(e) {
            // use the first two touches and their previous positions
            var t0 = e.touches[0],
                t1 = e.touches[1];
            p0.x = t0.clientX;
            p0.y = t0.clientY;
            p1.x = t1.clientX;
            p1.y = t1.clientY;
            l0 = locations[t0.identifier],
            l1 = locations[t1.identifier];

            // mark these touches so they aren't used as taps/holds
            l0.wasPinch = true;
            l1.wasPinch = true;

            // scale about the center of these touches
            var center = MM.Point.interpolate(p0, p1, 0.5);

            map.zoomByAbout(
                Math.log(e.scale) / Math.LN2 - Math.log(l0.scale) / Math.LN2,
                center);

            // pan from the previous center of these touches
            prevX = l0.x + (l1.x - l0.x) * 0.5;
            prevY = l0.y + (l1.y - l0.y) * 0.5;
            map.panBy(center.x - prevX,
                      center.y - prevY);
            wasPinching = true;
            lastPinchCenter = center;
        }

        // When a pinch event ends, round the zoom of the map.
        function onPinched(p) {
            // TODO: easing
            if (true) {
                var z = map.getZoom(), // current zoom
                tz = Math.round(z);     // target zoom
                map.zoomByAbout(tz - z, p);
                clearLocations();
            }
            wasPinching = false;
        }

        function touchEndMachine(e) {
            var now = new Date().getTime();

            // round zoom if we're done pinching
            if (e.touches.length === 0 && wasPinching) {
                onPinched(lastPinchCenter);
            }

            panner.up();

            // Look at each changed touch in turn.
            for (var i = 0; i < e.changedTouches.length; i += 1) {
                var t = e.changedTouches[i],
                loc = locations[t.identifier];
                // if we didn't see this one (bug?)
                // or if it was consumed by pinching already
                // just skip to the next one
                if (!loc || loc.wasPinch) {
                    continue;
                }

                // we now know we have an event object and a
                // matching touch that's just ended. Let's see
                // what kind of event it is based on how long it
                // lasted and how far it moved.
                var pos = { x: t.clientX, y: t.clientY },
                time = now - loc.time,
                travel = MM.Point.distance(pos, loc.startPos);
                if (travel > maxTapDistance) {
                    // we will to assume that the drag has been handled separately
                } else if (time > maxTapTime) {
                    // close in space, but not in time: a hold
                    pos.end = now;
                    pos.duration = time;
                    onHold(pos);
                } else {
                    // close in both time and space: a tap
                    pos.time = now;
                    onTap(pos);
                }
            }

            // Weird, sometimes an end event doesn't get thrown
            // for a touch that nevertheless has disappeared.
            // Still, this will eventually catch those ids:

            var validTouchIds = {};
            for (var j = 0; j < e.touches.length; j++) {
                validTouchIds[e.touches[j].identifier] = true;
            }
            for (var id in locations) {
                if (!(id in validTouchIds)) {
                    delete validTouchIds[id];
                }
            }

            return MM.cancelEvent(e);
        }

        handler.init = function(x) {
            map = x;
            // Fail early if this isn't a touch device.
            // TODO: move to add fn
            if (!isTouchable()) return false;

            MM.addEvent(map.parent, 'touchstart',
                touchStartMachine);
            MM.addEvent(map.parent, 'touchmove',
                touchMoveMachine);
            MM.addEvent(map.parent, 'touchend',
                touchEndMachine);

            panner = panning();
        };

        handler.remove = function() {
            // Fail early if this isn't a touch device.
            if (!isTouchable()) return false;

            MM.removeEvent(map.parent, 'touchstart',
                touchStartMachine);
            MM.removeEvent(map.parent, 'touchmove',
                touchMoveMachine);
            MM.removeEvent(map.parent, 'touchend',
                touchEndMachine);
            panner.remove();
        };

        return handler;
    };

    easey_handlers.DoubleClickHandler = function() {
        var handler = {},
            map;

        function doubleClick(e) {
            // Ensure that this handler is attached once.
            // Get the point on the map that was double-clicked
            var point = MM.getMousePoint(e, map);
            z = map.getZoom() + (e.shiftKey ? -1 : 1);
            // use shift-double-click to zoom out
            easey().map(map)
                .to(map.pointCoordinate(MM.getMousePoint(e, map)).zoomTo(z))
                .path('about').run(100, function() {
                map.dispatchCallback('zoomed');
            });
            return MM.cancelEvent(e);
        }

        handler.init = function(x) {
            map = x;
            MM.addEvent(map.parent, 'dblclick', doubleClick);
            return handler;
        };

        handler.remove = function() {
            MM.removeEvent(map.parent, 'dblclick', doubleClick);
        };

        return handler;
    };

    easey_handlers.MouseWheelHandler = function() {
        var handler = {},
            map,
            _zoomDiv,
            ea = easey(),
            prevTime,
            precise = false;

        function mouseWheel(e) {
            var delta = 0;
            prevTime = prevTime || new Date().getTime();

            try {
                _zoomDiv.scrollTop = 1000;
                _zoomDiv.dispatchEvent(e);
                delta = 1000 - _zoomDiv.scrollTop;
            } catch (error) {
                delta = e.wheelDelta || (-e.detail * 5);
            }

            // limit mousewheeling to once every 200ms
            var timeSince = new Date().getTime() - prevTime;

            function dispatchZoomed() {
                map.dispatchCallback('zoomed');
            }

            if (!ea.running()) {
              var point = MM.getMousePoint(e, map),
                  z = map.getZoom();
              ea.map(map)
                .easing('easeOut')
                .to(map.pointCoordinate(MM.getMousePoint(e, map)).zoomTo(z + (delta > 0 ? 1 : -1)))
                .path('about').run(100, dispatchZoomed);
                prevTime = new Date().getTime();
            } else if (timeSince > 150){
                ea.zoom(ea.to().zoom + (delta > 0 ? 1 : -1)).from(map.coordinate).resetRun();
                prevTime = new Date().getTime();
            }

            // Cancel the event so that the page doesn't scroll
            return MM.cancelEvent(e);
        }

        handler.init = function(x) {
            map = x;
            _zoomDiv = document.body.appendChild(document.createElement('div'));
            _zoomDiv.style.cssText = 'visibility:hidden;top:0;height:0;width:0;overflow-y:scroll';
            var innerDiv = _zoomDiv.appendChild(document.createElement('div'));
            innerDiv.style.height = '2000px';
            MM.addEvent(map.parent, 'mousewheel', mouseWheel);
            return handler;
        };

        handler.precise = function(x) {
            if (!arguments.length) return precise;
            precise = x;
            return handler;
        };

        handler.remove = function() {
            MM.removeEvent(map.parent, 'mousewheel', mouseWheel);
            _zoomDiv.parentNode.removeChild(_zoomDiv);
        };

        return handler;
    };

    easey_handlers.DragHandler = function() {
        var handler = {},
            map,
            panner;

        function focusMap(e) {
            map.parent.focus();
        }

        function mouseDown(e) {
            if (e.shiftKey || e.button == 2) return;
            MM.addEvent(document, 'mousemove', mouseMove);
            MM.addEvent(document, 'mouseup', mouseUp);
            panner.down(e);
            map.parent.style.cursor = 'move';
            return MM.cancelEvent(e);
        }

        function mouseMove(e) {
            panner.move(e);
            return MM.cancelEvent(e);
        }

        function mouseUp(e) {
            MM.removeEvent(document, 'mousemove', mouseMove);
            MM.removeEvent(document, 'mouseup', mouseUp);
            panner.up();
            map.parent.style.cursor = '';
            return MM.cancelEvent(e);
        }

        handler.init = function(x) {
            map = x;
            MM.addEvent(map.parent, 'click', focusMap);
            MM.addEvent(map.parent, 'mousedown', mouseDown);
            panner = panning();
        };

        handler.remove = function() {
            MM.removeEvent(map.parent, 'click', focusMap);
            MM.removeEvent(map.parent, 'mousedown', mouseDown);
            panner.up();
            panner.remove();
        };

        return handler;
    };

    function panning() {

        var p = {};

        var speed = { x: 0, y: 0 },
            dir = { x: 0, y: 0 },
            drag = 0.15,
            removed = false,
            nowPoint = null,
            oldPoint = null,
            moveTime = null,
            prevMoveTime = null,
            animatedLastPoint = true,
            t,
            prevT = new Date().getTime();

        p.down = function(e) {
            nowPoint = oldPoint = MM.getMousePoint(e, map);
            moveTime = prevMoveTime = +new Date();
        };

        p.move = function(e) {
            if (nowPoint) {
                if (animatedLastPoint) {
                    oldPoint = nowPoint;
                    prevMoveTime = moveTime;
                    animatedLastPoint = false;
                }
                nowPoint = MM.getMousePoint(e, map);
                moveTime = +new Date();
            }
        };

        p.up = function() {
            if (+new Date() - prevMoveTime < 50) {
                dt = Math.max(1, moveTime - prevMoveTime);
                dir.x = nowPoint.x - oldPoint.x;
                dir.y = nowPoint.y - oldPoint.y;
                speed.x = dir.x / dt;
                speed.y = dir.y / dt;
            } else {
                speed.x = 0;
                speed.y = 0;
            }
            nowPoint = oldPoint = null;
            moveTime = null;
        };

        p.remove = function() {
            removed = true;
        };

        function animate(t) {
            var dt = Math.max(1, t - prevT);
            if (nowPoint && oldPoint) {
                if (!animatedLastPoint) {
                    dir.x = nowPoint.x - oldPoint.x;
                    dir.y = nowPoint.y - oldPoint.y;
                    map.panBy(dir.x, dir.y);
                    animatedLastPoint = true;
                }
            } else {
                // Rough time based animation accuracy
                // using a linear approximation approach
                speed.x *= Math.pow(1 - drag, dt * 60 / 1000);
                speed.y *= Math.pow(1 - drag, dt * 60 / 1000);
                if (Math.abs(speed.x) < 0.001) {
                    speed.x = 0;
                }
                if (Math.abs(speed.y) < 0.001) {
                    speed.y = 0;
                }
                if (speed.x || speed.y) {
                    map.panBy(speed.x * dt, speed.y * dt);
                }
            }
            prevT = t;
            if (!removed) MM.getFrame(animate);
        }

        MM.getFrame(animate);
        return p;
    }

    this.easey_handlers = easey_handlers;

})(this, MM);
<commit_msg>Fix pinching touchend bug<commit_after>;(function(context, MM) {

    var easey_handlers = {};

    easey_handlers.TouchHandler = function() {
        var handler = {},
            map,
            panner,
            maxTapTime = 250,
            maxTapDistance = 30,
            maxDoubleTapDelay = 350,
            locations = {},
            taps = [],
            wasPinching = false,
            lastPinchCenter = null,
            p0 = new MM.Point(0, 0),
            p1 = new MM.Point(0, 0);

        function focusMap(e) {
            map.parent.focus();
        }

        function clearLocations() {
            for (var loc in locations) {
                if (locations.hasOwnProperty(loc)) {
                    delete locations[loc];
                }
            }
        }

        function updateTouches (e) {
            for (var i = 0; i < e.touches.length; i += 1) {
                var t = e.touches[i];
                if (t.identifier in locations) {
                    var l = locations[t.identifier];
                    l.x = t.clientX;
                    l.y = t.clientY;
                    l.scale = e.scale;
                } else {
                    locations[t.identifier] = {
                        scale: e.scale,
                        startPos: { x: t.clientX, y: t.screenY },
                        x: t.clientX,
                        y: t.clientY,
                        time: new Date().getTime()
                    };
                }
            }
        }

        function touchStartMachine(e) {
            MM.addEvent(e.touches[0].target, 'touchmove',
                touchMoveMachine);
            MM.addEvent(e.touches[0].target, 'touchend',
                touchEndMachine);
            if (e.touches[1]) {
                MM.addEvent(e.touches[1].target, 'touchmove',
                    touchMoveMachine);
                MM.addEvent(e.touches[1].target, 'touchend',
                    touchEndMachine);
            }
            updateTouches(e);
            panner.down(e.touches[0]);
            return MM.cancelEvent(e);
        }

        function touchMoveMachine(e) {
            switch (e.touches.length) {
                case 1:
                    panner.move(e.touches[0]);
                    break;
                case 2:
                    onPinching(e);
                    break;
            }
            updateTouches(e);
            return MM.cancelEvent(e);
        }

        // Handle a tap event - mainly watch for a doubleTap
        function onTap(tap) {
            if (taps.length &&
                (tap.time - taps[0].time) < maxDoubleTapDelay) {
                onDoubleTap(tap);
                taps = [];
                return;
            }
            taps = [tap];
        }

        // Handle a double tap by zooming in a single zoom level to a
        // round zoom.
        function onDoubleTap(tap) {
            // zoom in to a round number
            easey().map(map)
            .to(map.pointCoordinate(tap).zoomTo(map.getZoom() + 1))
            .path('about').run(200, function() {
                map.dispatchCallback('zoomed');
                clearLocations();
            });
        }

        function isTouchable() {
            var el = document.createElement('div');
            el.setAttribute('ongesturestart', 'return;');
            return (typeof el.ongesturestart === 'function');
        }

        function onPinching(e) {
            // use the first two touches and their previous positions
            var t0 = e.touches[0],
                t1 = e.touches[1];
            p0.x = t0.clientX;
            p0.y = t0.clientY;
            p1.x = t1.clientX;
            p1.y = t1.clientY;
            l0 = locations[t0.identifier],
            l1 = locations[t1.identifier];

            // mark these touches so they aren't used as taps/holds
            l0.wasPinch = true;
            l1.wasPinch = true;

            // scale about the center of these touches
            var center = MM.Point.interpolate(p0, p1, 0.5);

            map.zoomByAbout(
                Math.log(e.scale) / Math.LN2 - Math.log(l0.scale) / Math.LN2,
                center);

            // pan from the previous center of these touches
            prevX = l0.x + (l1.x - l0.x) * 0.5;
            prevY = l0.y + (l1.y - l0.y) * 0.5;
            map.panBy(center.x - prevX,
                      center.y - prevY);
            wasPinching = true;
            lastPinchCenter = center;
        }

        // When a pinch event ends, round the zoom of the map.
        function onPinched(p) {
            // TODO: easing
            var z = map.getZoom(), // current zoom
            tz = Math.round(z);     // target zoom
            map.zoomByAbout(tz - z, p);
            clearLocations();
            wasPinching = false;
        }

        function touchEndMachine(e) {
            MM.removeEvent(e.target, 'touchmove',
                touchMoveMachine);
            MM.removeEvent(e.target, 'touchend',
                touchEndMachine);
            var now = new Date().getTime();

            // round zoom if we're done pinching
            if (e.touches.length === 0 && wasPinching) {
                onPinched(lastPinchCenter);
            }

            panner.up();

            // Look at each changed touch in turn.
            for (var i = 0; i < e.changedTouches.length; i += 1) {
                var t = e.changedTouches[i],
                loc = locations[t.identifier];
                // if we didn't see this one (bug?)
                // or if it was consumed by pinching already
                // just skip to the next one
                if (!loc || loc.wasPinch) {
                    continue;
                }

                // we now know we have an event object and a
                // matching touch that's just ended. Let's see
                // what kind of event it is based on how long it
                // lasted and how far it moved.
                var pos = { x: t.clientX, y: t.clientY },
                time = now - loc.time,
                travel = MM.Point.distance(pos, loc.startPos);
                if (travel > maxTapDistance) {
                    // we will to assume that the drag has been handled separately
                } else if (time > maxTapTime) {
                    // close in space, but not in time: a hold
                    pos.end = now;
                    pos.duration = time;
                    onHold(pos);
                } else {
                    // close in both time and space: a tap
                    pos.time = now;
                    onTap(pos);
                }
            }

            // Weird, sometimes an end event doesn't get thrown
            // for a touch that nevertheless has disappeared.
            // Still, this will eventually catch those ids:

            var validTouchIds = {};
            for (var j = 0; j < e.touches.length; j++) {
                validTouchIds[e.touches[j].identifier] = true;
            }
            for (var id in locations) {
                if (!(id in validTouchIds)) {
                    delete validTouchIds[id];
                }
            }

            return MM.cancelEvent(e);
        }

        handler.init = function(x) {
            map = x;
            // Fail early if this isn't a touch device.
            // TODO: move to add fn
            if (!isTouchable()) return false;

            MM.addEvent(map.parent, 'touchstart',
                touchStartMachine);
            panner = panning(0.10);
        };

        handler.remove = function() {
            // Fail early if this isn't a touch device.
            if (!isTouchable()) return false;

            MM.removeEvent(map.parent, 'touchstart',
                touchStartMachine);
            panner.remove();
        };

        return handler;
    };

    easey_handlers.DoubleClickHandler = function() {
        var handler = {},
            map;

        function doubleClick(e) {
            // Ensure that this handler is attached once.
            // Get the point on the map that was double-clicked
            var point = MM.getMousePoint(e, map);
            z = map.getZoom() + (e.shiftKey ? -1 : 1);
            // use shift-double-click to zoom out
            easey().map(map)
                .to(map.pointCoordinate(MM.getMousePoint(e, map)).zoomTo(z))
                .path('about').run(100, function() {
                map.dispatchCallback('zoomed');
            });
            return MM.cancelEvent(e);
        }

        handler.init = function(x) {
            map = x;
            MM.addEvent(map.parent, 'dblclick', doubleClick);
            return handler;
        };

        handler.remove = function() {
            MM.removeEvent(map.parent, 'dblclick', doubleClick);
        };

        return handler;
    };

    easey_handlers.MouseWheelHandler = function() {
        var handler = {},
            map,
            _zoomDiv,
            ea = easey(),
            prevTime,
            precise = false;

        function mouseWheel(e) {
            var delta = 0;
            prevTime = prevTime || new Date().getTime();

            try {
                _zoomDiv.scrollTop = 1000;
                _zoomDiv.dispatchEvent(e);
                delta = 1000 - _zoomDiv.scrollTop;
            } catch (error) {
                delta = e.wheelDelta || (-e.detail * 5);
            }

            // limit mousewheeling to once every 200ms
            var timeSince = new Date().getTime() - prevTime;

            function dispatchZoomed() {
                map.dispatchCallback('zoomed');
            }

            if (!ea.running()) {
              var point = MM.getMousePoint(e, map),
                  z = map.getZoom();
              ea.map(map)
                .easing('easeOut')
                .to(map.pointCoordinate(MM.getMousePoint(e, map)).zoomTo(z + (delta > 0 ? 1 : -1)))
                .path('about').run(100, dispatchZoomed);
                prevTime = new Date().getTime();
            } else if (timeSince > 150){
                ea.zoom(ea.to().zoom + (delta > 0 ? 1 : -1)).from(map.coordinate).resetRun();
                prevTime = new Date().getTime();
            }

            // Cancel the event so that the page doesn't scroll
            return MM.cancelEvent(e);
        }

        handler.init = function(x) {
            map = x;
            _zoomDiv = document.body.appendChild(document.createElement('div'));
            _zoomDiv.style.cssText = 'visibility:hidden;top:0;height:0;width:0;overflow-y:scroll';
            var innerDiv = _zoomDiv.appendChild(document.createElement('div'));
            innerDiv.style.height = '2000px';
            MM.addEvent(map.parent, 'mousewheel', mouseWheel);
            return handler;
        };

        handler.precise = function(x) {
            if (!arguments.length) return precise;
            precise = x;
            return handler;
        };

        handler.remove = function() {
            MM.removeEvent(map.parent, 'mousewheel', mouseWheel);
            _zoomDiv.parentNode.removeChild(_zoomDiv);
        };

        return handler;
    };

    easey_handlers.DragHandler = function() {
        var handler = {},
            map,
            panner;

        function focusMap(e) {
            map.parent.focus();
        }

        function mouseDown(e) {
            if (e.shiftKey || e.button == 2) return;
            MM.addEvent(document, 'mousemove', mouseMove);
            MM.addEvent(document, 'mouseup', mouseUp);
            panner.down(e);
            map.parent.style.cursor = 'move';
            return MM.cancelEvent(e);
        }

        function mouseMove(e) {
            panner.move(e);
            return MM.cancelEvent(e);
        }

        function mouseUp(e) {
            MM.removeEvent(document, 'mousemove', mouseMove);
            MM.removeEvent(document, 'mouseup', mouseUp);
            panner.up();
            map.parent.style.cursor = '';
            return MM.cancelEvent(e);
        }

        handler.init = function(x) {
            map = x;
            MM.addEvent(map.parent, 'click', focusMap);
            MM.addEvent(map.parent, 'mousedown', mouseDown);
            panner = panning();
        };

        handler.remove = function() {
            MM.removeEvent(map.parent, 'click', focusMap);
            MM.removeEvent(map.parent, 'mousedown', mouseDown);
            panner.up();
            panner.remove();
        };

        return handler;
    };

    function panning(drag) {

        var p = {};
        drag = drag || 0.15;

        var speed = { x: 0, y: 0 },
            dir = { x: 0, y: 0 },
            removed = false,
            nowPoint = null,
            oldPoint = null,
            moveTime = null,
            prevMoveTime = null,
            animatedLastPoint = true,
            t,
            prevT = new Date().getTime();

        p.down = function(e) {
            nowPoint = oldPoint = MM.getMousePoint(e, map);
            moveTime = prevMoveTime = +new Date();
        };

        p.move = function(e) {
            if (nowPoint) {
                if (animatedLastPoint) {
                    oldPoint = nowPoint;
                    prevMoveTime = moveTime;
                    animatedLastPoint = false;
                }
                nowPoint = MM.getMousePoint(e, map);
                moveTime = +new Date();
            }
        };

        p.up = function() {
            if (+new Date() - prevMoveTime < 50) {
                dt = Math.max(1, moveTime - prevMoveTime);
                dir.x = nowPoint.x - oldPoint.x;
                dir.y = nowPoint.y - oldPoint.y;
                speed.x = dir.x / dt;
                speed.y = dir.y / dt;
            } else {
                speed.x = 0;
                speed.y = 0;
            }
            nowPoint = oldPoint = null;
            moveTime = null;
        };

        p.remove = function() {
            removed = true;
        };

        function animate(t) {
            var dt = Math.max(1, t - prevT);
            if (nowPoint && oldPoint) {
                if (!animatedLastPoint) {
                    dir.x = nowPoint.x - oldPoint.x;
                    dir.y = nowPoint.y - oldPoint.y;
                    map.panBy(dir.x, dir.y);
                    animatedLastPoint = true;
                }
            } else {
                // Rough time based animation accuracy
                // using a linear approximation approach
                speed.x *= Math.pow(1 - drag, dt * 60 / 1000);
                speed.y *= Math.pow(1 - drag, dt * 60 / 1000);
                if (Math.abs(speed.x) < 0.001) {
                    speed.x = 0;
                }
                if (Math.abs(speed.y) < 0.001) {
                    speed.y = 0;
                }
                if (speed.x || speed.y) {
                    map.panBy(speed.x * dt, speed.y * dt);
                }
            }
            prevT = t;
            if (!removed) MM.getFrame(animate);
        }

        MM.getFrame(animate);
        return p;
    }

    this.easey_handlers = easey_handlers;

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