<commit_before>'use strict';

var ajvErrors = require('../index');
var Ajv = require('ajv');
var assert = require('assert');

describe('errorMessage value is an object', function() {
  var ajvs;

  beforeEach(function() {
    ajvs = [
      ajvErrors(new Ajv({allErrors: true, jsonPointers: true/*, sourceCode: true, processCode: require('js-beautify').js_beautify*/ })),
      ajvErrors(new Ajv({allErrors: true, jsonPointers: true, verbose: true/*, sourceCode: true, processCode: require('js-beautify').js_beautify*/ }))
    ];
  });

  describe('keywords', function() {
    it('should replace keyword errors with custom error messages', function() {
      var schema = {
        type: 'object',
        required: ['foo'],
        properties: {
          foo: { type: 'integer' }
        },
        additionalProperties: false,
        errorMessage: {
          type: 'should be an object',
          required: 'should have property foo',
          additionalProperties: 'should not have properties other than foo'
        }
      };

      ajvs.forEach(function (ajv) {
        var validate = ajv.compile(schema);
        assert.strictEqual(validate({foo: 1}), true);
        testInvalid({},                 [['required']]);
        testInvalid({bar: 2},           [['required'], ['additionalProperties']]);
        testInvalid({foo: 1, bar: 2},   [['additionalProperties']]);
        testInvalid({foo: 'a'},         ['type']);
        testInvalid({foo: 'a', bar: 2}, ['type', ['additionalProperties']]);
        testInvalid(1,                  [['type']]);

        function testInvalid(data, expectedErrors) {
          assert.strictEqual(validate(data), false);
          assert.strictEqual(validate.errors.length, expectedErrors.length);
          validate.errors.forEach(function (err, i) {
            var expectedErr = expectedErrors[i];
            if (Array.isArray(expectedErr)) { // errorMessage error
              assert.strictEqual(err.keyword, 'errorMessage');
              assert.strictEqual(err.message, schema.errorMessage[err.params.errors[0].keyword]);
              assert.strictEqual(err.dataPath, '');
              assert.strictEqual(err.schemaPath, '#/errorMessage');
              var replacedKeywords = err.params.errors.map(function (e) {
                return e.keyword;
              });
              assert.deepEqual(replacedKeywords.sort(), expectedErr.sort());
            } else { // original error
              assert.strictEqual(err.keyword, expectedErr);
            }
          });
        }
      });
    });

    it('should replace keyword errors with interpolated error messages', function() {
      var schema = {
        type: 'object',
        properties: {
          foo: {
            type: 'number',
            minimum: 5,
            maximum: 10,
            errorMessage: {
              type: 'property ${0#} should be number, it is ${0}',
              minimum: 'property ${0#} should be >= 5, it is ${0}',
              maximum: 'property foo should be <= 10'
            }
          }
        }
      };

      ajvs.forEach(function (ajv) {
        var validate = ajv.compile(schema);
        assert.strictEqual(validate({foo: 7}), true);
        testInvalid({foo: 'a'},   [['type']]);
        testInvalid({foo: ['a']}, [['type']]);
        testInvalid({foo: 4.5},   [['minimum']]);
        testInvalid({foo: 10.5},  [['maximum']]);

        function testInvalid(data, expectedErrors) {
          assert.strictEqual(validate(data), false);
          assert.strictEqual(validate.errors.length, expectedErrors.length);
          validate.errors.forEach(function (err, i) {
            var expectedErr = expectedErrors[i];
            if (Array.isArray(expectedErr)) { // errorMessage error
              assert.strictEqual(err.keyword, 'errorMessage');
              var expectedMessage = schema.properties.foo
                                    .errorMessage[err.params.errors[0].keyword]
                                    .replace('${0#}', '"foo"')
                                    .replace('${0}', JSON.stringify(data.foo));
              assert.strictEqual(err.message, expectedMessage);
              assert.strictEqual(err.dataPath, '/foo');
              assert.strictEqual(err.schemaPath, '#/properties/foo/errorMessage');
              var replacedKeywords = err.params.errors.map(function (e) {
                return e.keyword;
              });
              assert.deepEqual(replacedKeywords.sort(), expectedErr.sort());
            } else { // original error
              assert.strictEqual(err.keyword, expectedErr);
            }
          });
        }
      });
    });
  });

  describe('properties and items', function() {
    var schema, validate;

    it('should replace errors for properties with custom error messages', function() {
      schema = {
        type: 'object',
        required: ['foo', 'bar'],
        properties: {
          foo: {
            type: 'object',
            required: ['baz'],
            properties: {
              baz: {type: 'integer', maximum: 2}
            }
          },
          bar: {
            type: 'array',
            items: {type: 'string', maxLength: 3},
            minItems: 1
          }
        },
        additionalProperties: false,
        errorMessage: {
          properties: {
            foo: 'data.foo should be an object with the integer property "baz" <= 2',
            bar: 'data.bar should be an array with at least one string item with length <= 3'
          }
        }
      };

      var validData = {
        foo: {baz: 1},
        bar: ['abc']
      };

      ajvs.forEach(function (ajv) {
        validate = ajv.compile(schema);

        assert.strictEqual(validate(validData), true);
        testInvalid({},                 ['required', 'required']);
        testInvalid({foo: 1},           ['required', ['type']]);
        testInvalid({foo: 1, bar: 2},   [['type'], ['type']]);
        testInvalid({foo: {baz: 'a'}},  ['required', ['type']]);
        testInvalid({foo: {baz: 3}},    ['required', ['maximum']]);
        testInvalid({foo: {baz: 3}, bar: []},       [['maximum'], ['minItems']]);
        testInvalid({foo: {baz: 3}, bar: [1]},      [['maximum'], ['type']]);
        testInvalid({foo: {baz: 3}, bar: ['abcd']}, [['maximum'], ['maxLength']]);
      });
    });

    it('should replace errors for items with custom error messages', function() {
      schema = {
        type: 'array',
        items: [
          {
            type: 'object',
            required: ['baz'],
            properties: {
              baz: {type: 'integer', maximum: 2}
            }
          },
          {
            type: 'array',
            items: {type: 'string', maxLength: 3},
            minItems: 1
          }
        ],
        minItems: 2,
        additionalItems: false,
        errorMessage: {
          items: [
            'data[0] should be an object with the integer property "baz" <= 2',
            'data[1] should be an array with at least one string item with length <= 3'
          ]
        }
      };

      var validData = [
        {baz: 1},
        ['abc']
      ];

      ajvs.forEach(function (ajv) {
        validate = ajv.compile(schema);

        assert.strictEqual(validate(validData), true);
        testInvalid([],            ['minItems']);
        testInvalid([1],           ['minItems', ['type']]);
        testInvalid([1, 2],        [['type'], ['type']]);
        testInvalid([{baz: 'a'}],  ['minItems', ['type']]);
        testInvalid([{baz: 3}],    ['minItems', ['maximum']]);
        testInvalid([{baz: 3}, []],       [['maximum'], ['minItems']]);
        testInvalid([{baz: 3}, [1]],      [['maximum'], ['type']]);
        testInvalid([{baz: 3}, ['abcd']], [['maximum'], ['maxLength']]);
      });
    });

    it('should replace errors for properties and items with custom error messages', function() {
      schema = {
        definitions: {
          foo: {
            type: 'object',
            required: ['baz'],
            properties: {
              baz: {type: 'integer', maximum: 2}
            }
          },
          bar: {
            type: 'array',
            items: {type: 'string', maxLength: 3},
            minItems: 1
          }
        },
        anyOf: [
          {
            type: 'object',
            required: ['foo', 'bar'],
            properties: {
              foo: {$ref: '#/definitions/foo'},
              bar: {$ref: '#/definitions/bar'}
            },
            additionalProperties: false
          },
          {
            type: 'array',
            items: [
              {$ref: '#/definitions/foo'},
              {$ref: '#/definitions/bar'}
            ],
            minItems: 2,
            additionalItems: false
          }
        ],
        errorMessage: {
          properties: {
            foo: 'data.foo should be an object with the integer property "baz" <= 2',
            bar: 'data.bar should be an array with at least one string item with length <= 3'
          },
          items: [
            'data[0] should be an object with the integer property "baz" <= 2',
            'data[1] should be an array with at least one string item with length <= 3'
          ]
        }
      };

      var validData1 = {
        foo: {baz: 1},
        bar: ['abc']
      };

      var validData2 = [
        {baz: 1},
        ['abc']
      ];

      ajvs.forEach(function (ajv) {
        validate = ajv.compile(schema);

        assert.strictEqual(validate(validData1), true);
        assert.strictEqual(validate(validData2), true);
        testInvalid({},                 ['required', 'required', 'type', 'anyOf']);
        testInvalid({foo: 1},           ['required', 'type', 'anyOf', ['type']]);
        testInvalid({foo: 1, bar: 2},   ['type', 'anyOf', ['type'], ['type']]);
        testInvalid({foo: {baz: 'a'}},  ['required', 'type', 'anyOf', ['type']]);
        testInvalid({foo: {baz: 3}},    ['required', 'type', 'anyOf', ['maximum']]);
        testInvalid({foo: {baz: 3}, bar: []},       ['type', 'anyOf', ['maximum'], ['minItems']]);
        testInvalid({foo: {baz: 3}, bar: [1]},      ['type', 'anyOf', ['maximum'], ['type']]);
        testInvalid({foo: {baz: 3}, bar: ['abcd']}, ['type', 'anyOf', ['maximum'], ['maxLength']]);

        testInvalid([],            ['type', 'minItems', 'anyOf']);
        testInvalid([1],           ['type', 'minItems', 'anyOf', ['type']]);
        testInvalid([1, 2],        ['type', 'anyOf', ['type'], ['type']]);
        testInvalid([{baz: 'a'}],  ['type', 'minItems', 'anyOf', ['type']]);
        testInvalid([{baz: 3}],    ['type', 'minItems', 'anyOf', ['maximum']]);
        testInvalid([{baz: 3}, []],       ['type', 'anyOf', ['maximum'], ['minItems']]);
        testInvalid([{baz: 3}, [1]],      ['type', 'anyOf', ['maximum'], ['type']]);
        testInvalid([{baz: 3}, ['abcd']], ['type', 'anyOf', ['maximum'], ['maxLength']]);
      });
    });

    it('should replace errors for properties with interpolated error messages', function() {
      schema = {
        type: 'object',
        required: ['foo', 'bar'],
        properties: {
          foo: {
            type: 'object',
            required: ['baz'],
            properties: {
              baz: {type: 'integer', maximum: 2}
            }
          },
          bar: {
            type: 'array',
            items: {type: 'string', maxLength: 3},
            minItems: 1
          }
        },
        additionalProperties: false,
        errorMessage: {
          properties: {
            foo: 'data.foo should be an object with the integer property "baz" <= 2, "baz" is ${/foo/baz}',
            bar: 'data.bar should be an array with at least one string item with length <= 3, "bar" is ${/bar}'
          }
        }
      };

      var validData = {
        foo: {baz: 1},
        bar: ['abc']
      };

      ajvs.forEach(function (ajv) {
        validate = ajv.compile(schema);

        assert.strictEqual(validate(validData), true);
        testInvalid({},                 ['required', 'required'], tmpl);
        testInvalid({foo: 1},           ['required', ['type']], tmpl);
        testInvalid({foo: 1, bar: 2},   [['type'], ['type']], tmpl);
        testInvalid({foo: {baz: 'a'}},  ['required', ['type']], tmpl);
        testInvalid({foo: {baz: 3}},    ['required', ['maximum']], tmpl);
        testInvalid({foo: {baz: 3}, bar: []},       [['maximum'], ['minItems']], tmpl);
        testInvalid({foo: {baz: 3}, bar: [1]},      [['maximum'], ['type']], tmpl);
        testInvalid({foo: {baz: 3}, bar: ['abcd']}, [['maximum'], ['maxLength']], tmpl);
      });

      function tmpl(str, data) {
        return str.replace('${/foo/baz}', JSON.stringify(data.foo && data.foo.baz))
                  .replace('${/bar}', JSON.stringify(data.bar));

      }
    });

    function testInvalid(data, expectedErrors, interpolate) {
      assert.strictEqual(validate(data), false);
      assert.strictEqual(validate.errors.length, expectedErrors.length);
      validate.errors.forEach(function (err, i) {
        var expectedErr = expectedErrors[i];
        if (Array.isArray(expectedErr)) { // errorMessage error
          assert.strictEqual(err.keyword, 'errorMessage');
          var child = Array.isArray(data) ? 'items' : 'properties';
          var expectedMessage = schema.errorMessage[child][err.dataPath.slice(1)];
          if (interpolate) expectedMessage = interpolate(expectedMessage, data);
          assert.strictEqual(err.message, expectedMessage);
          assert((Array.isArray(data) ? /^\/(0|1)$/ : /^\/(foo|bar)$/).test(err.dataPath));
          assert.strictEqual(err.schemaPath, '#/errorMessage');
          var replacedKeywords = err.params.errors.map(function (e) {
            return e.keyword;
          });
          assert.deepEqual(replacedKeywords.sort(), expectedErr.sort());
        } else { // original error
          assert.strictEqual(err.keyword, expectedErr);
        }
      });
    }
  });
});
<commit_msg>test: interpolated messages for items, #4<commit_after>'use strict';

var ajvErrors = require('../index');
var Ajv = require('ajv');
var assert = require('assert');

describe('errorMessage value is an object', function() {
  var ajvs;

  beforeEach(function() {
    ajvs = [
      ajvErrors(new Ajv({allErrors: true, jsonPointers: true/*, sourceCode: true, processCode: require('js-beautify').js_beautify*/ })),
      ajvErrors(new Ajv({allErrors: true, jsonPointers: true, verbose: true/*, sourceCode: true, processCode: require('js-beautify').js_beautify*/ }))
    ];
  });

  describe('keywords', function() {
    it('should replace keyword errors with custom error messages', function() {
      var schema = {
        type: 'object',
        required: ['foo'],
        properties: {
          foo: { type: 'integer' }
        },
        additionalProperties: false,
        errorMessage: {
          type: 'should be an object',
          required: 'should have property foo',
          additionalProperties: 'should not have properties other than foo'
        }
      };

      ajvs.forEach(function (ajv) {
        var validate = ajv.compile(schema);
        assert.strictEqual(validate({foo: 1}), true);
        testInvalid({},                 [['required']]);
        testInvalid({bar: 2},           [['required'], ['additionalProperties']]);
        testInvalid({foo: 1, bar: 2},   [['additionalProperties']]);
        testInvalid({foo: 'a'},         ['type']);
        testInvalid({foo: 'a', bar: 2}, ['type', ['additionalProperties']]);
        testInvalid(1,                  [['type']]);

        function testInvalid(data, expectedErrors) {
          assert.strictEqual(validate(data), false);
          assert.strictEqual(validate.errors.length, expectedErrors.length);
          validate.errors.forEach(function (err, i) {
            var expectedErr = expectedErrors[i];
            if (Array.isArray(expectedErr)) { // errorMessage error
              assert.strictEqual(err.keyword, 'errorMessage');
              assert.strictEqual(err.message, schema.errorMessage[err.params.errors[0].keyword]);
              assert.strictEqual(err.dataPath, '');
              assert.strictEqual(err.schemaPath, '#/errorMessage');
              var replacedKeywords = err.params.errors.map(function (e) {
                return e.keyword;
              });
              assert.deepEqual(replacedKeywords.sort(), expectedErr.sort());
            } else { // original error
              assert.strictEqual(err.keyword, expectedErr);
            }
          });
        }
      });
    });

    it('should replace keyword errors with interpolated error messages', function() {
      var schema = {
        type: 'object',
        properties: {
          foo: {
            type: 'number',
            minimum: 5,
            maximum: 10,
            errorMessage: {
              type: 'property ${0#} should be number, it is ${0}',
              minimum: 'property ${0#} should be >= 5, it is ${0}',
              maximum: 'property foo should be <= 10'
            }
          }
        }
      };

      ajvs.forEach(function (ajv) {
        var validate = ajv.compile(schema);
        assert.strictEqual(validate({foo: 7}), true);
        testInvalid({foo: 'a'},   [['type']]);
        testInvalid({foo: ['a']}, [['type']]);
        testInvalid({foo: 4.5},   [['minimum']]);
        testInvalid({foo: 10.5},  [['maximum']]);

        function testInvalid(data, expectedErrors) {
          assert.strictEqual(validate(data), false);
          assert.strictEqual(validate.errors.length, expectedErrors.length);
          validate.errors.forEach(function (err, i) {
            var expectedErr = expectedErrors[i];
            if (Array.isArray(expectedErr)) { // errorMessage error
              assert.strictEqual(err.keyword, 'errorMessage');
              var expectedMessage = schema.properties.foo
                                    .errorMessage[err.params.errors[0].keyword]
                                    .replace('${0#}', '"foo"')
                                    .replace('${0}', JSON.stringify(data.foo));
              assert.strictEqual(err.message, expectedMessage);
              assert.strictEqual(err.dataPath, '/foo');
              assert.strictEqual(err.schemaPath, '#/properties/foo/errorMessage');
              var replacedKeywords = err.params.errors.map(function (e) {
                return e.keyword;
              });
              assert.deepEqual(replacedKeywords.sort(), expectedErr.sort());
            } else { // original error
              assert.strictEqual(err.keyword, expectedErr);
            }
          });
        }
      });
    });
  });

  describe('properties and items', function() {
    var schema, validate;

    it('should replace errors for properties with custom error messages', function() {
      schema = {
        type: 'object',
        required: ['foo', 'bar'],
        properties: {
          foo: {
            type: 'object',
            required: ['baz'],
            properties: {
              baz: {type: 'integer', maximum: 2}
            }
          },
          bar: {
            type: 'array',
            items: {type: 'string', maxLength: 3},
            minItems: 1
          }
        },
        additionalProperties: false,
        errorMessage: {
          properties: {
            foo: 'data.foo should be an object with the integer property "baz" <= 2',
            bar: 'data.bar should be an array with at least one string item with length <= 3'
          }
        }
      };

      var validData = {
        foo: {baz: 1},
        bar: ['abc']
      };

      ajvs.forEach(function (ajv) {
        validate = ajv.compile(schema);

        assert.strictEqual(validate(validData), true);
        testInvalid({},                 ['required', 'required']);
        testInvalid({foo: 1},           ['required', ['type']]);
        testInvalid({foo: 1, bar: 2},   [['type'], ['type']]);
        testInvalid({foo: {baz: 'a'}},  ['required', ['type']]);
        testInvalid({foo: {baz: 3}},    ['required', ['maximum']]);
        testInvalid({foo: {baz: 3}, bar: []},       [['maximum'], ['minItems']]);
        testInvalid({foo: {baz: 3}, bar: [1]},      [['maximum'], ['type']]);
        testInvalid({foo: {baz: 3}, bar: ['abcd']}, [['maximum'], ['maxLength']]);
      });
    });

    it('should replace errors for items with custom error messages', function() {
      schema = {
        type: 'array',
        items: [
          {
            type: 'object',
            required: ['baz'],
            properties: {
              baz: {type: 'integer', maximum: 2}
            }
          },
          {
            type: 'array',
            items: {type: 'string', maxLength: 3},
            minItems: 1
          }
        ],
        minItems: 2,
        additionalItems: false,
        errorMessage: {
          items: [
            'data[0] should be an object with the integer property "baz" <= 2',
            'data[1] should be an array with at least one string item with length <= 3'
          ]
        }
      };

      var validData = [
        {baz: 1},
        ['abc']
      ];

      ajvs.forEach(function (ajv) {
        validate = ajv.compile(schema);

        assert.strictEqual(validate(validData), true);
        testInvalid([],            ['minItems']);
        testInvalid([1],           ['minItems', ['type']]);
        testInvalid([1, 2],        [['type'], ['type']]);
        testInvalid([{baz: 'a'}],  ['minItems', ['type']]);
        testInvalid([{baz: 3}],    ['minItems', ['maximum']]);
        testInvalid([{baz: 3}, []],       [['maximum'], ['minItems']]);
        testInvalid([{baz: 3}, [1]],      [['maximum'], ['type']]);
        testInvalid([{baz: 3}, ['abcd']], [['maximum'], ['maxLength']]);
      });
    });

    it('should replace errors for properties and items with custom error messages', function() {
      schema = {
        definitions: {
          foo: {
            type: 'object',
            required: ['baz'],
            properties: {
              baz: {type: 'integer', maximum: 2}
            }
          },
          bar: {
            type: 'array',
            items: {type: 'string', maxLength: 3},
            minItems: 1
          }
        },
        anyOf: [
          {
            type: 'object',
            required: ['foo', 'bar'],
            properties: {
              foo: {$ref: '#/definitions/foo'},
              bar: {$ref: '#/definitions/bar'}
            },
            additionalProperties: false
          },
          {
            type: 'array',
            items: [
              {$ref: '#/definitions/foo'},
              {$ref: '#/definitions/bar'}
            ],
            minItems: 2,
            additionalItems: false
          }
        ],
        errorMessage: {
          properties: {
            foo: 'data.foo should be an object with the integer property "baz" <= 2',
            bar: 'data.bar should be an array with at least one string item with length <= 3'
          },
          items: [
            'data[0] should be an object with the integer property "baz" <= 2',
            'data[1] should be an array with at least one string item with length <= 3'
          ]
        }
      };

      var validData1 = {
        foo: {baz: 1},
        bar: ['abc']
      };

      var validData2 = [
        {baz: 1},
        ['abc']
      ];

      ajvs.forEach(function (ajv) {
        validate = ajv.compile(schema);

        assert.strictEqual(validate(validData1), true);
        assert.strictEqual(validate(validData2), true);
        testInvalid({},                 ['required', 'required', 'type', 'anyOf']);
        testInvalid({foo: 1},           ['required', 'type', 'anyOf', ['type']]);
        testInvalid({foo: 1, bar: 2},   ['type', 'anyOf', ['type'], ['type']]);
        testInvalid({foo: {baz: 'a'}},  ['required', 'type', 'anyOf', ['type']]);
        testInvalid({foo: {baz: 3}},    ['required', 'type', 'anyOf', ['maximum']]);
        testInvalid({foo: {baz: 3}, bar: []},       ['type', 'anyOf', ['maximum'], ['minItems']]);
        testInvalid({foo: {baz: 3}, bar: [1]},      ['type', 'anyOf', ['maximum'], ['type']]);
        testInvalid({foo: {baz: 3}, bar: ['abcd']}, ['type', 'anyOf', ['maximum'], ['maxLength']]);

        testInvalid([],            ['type', 'minItems', 'anyOf']);
        testInvalid([1],           ['type', 'minItems', 'anyOf', ['type']]);
        testInvalid([1, 2],        ['type', 'anyOf', ['type'], ['type']]);
        testInvalid([{baz: 'a'}],  ['type', 'minItems', 'anyOf', ['type']]);
        testInvalid([{baz: 3}],    ['type', 'minItems', 'anyOf', ['maximum']]);
        testInvalid([{baz: 3}, []],       ['type', 'anyOf', ['maximum'], ['minItems']]);
        testInvalid([{baz: 3}, [1]],      ['type', 'anyOf', ['maximum'], ['type']]);
        testInvalid([{baz: 3}, ['abcd']], ['type', 'anyOf', ['maximum'], ['maxLength']]);
      });
    });

    it('should replace errors for properties with interpolated error messages', function() {
      schema = {
        type: 'object',
        required: ['foo', 'bar'],
        properties: {
          foo: {
            type: 'object',
            required: ['baz'],
            properties: {
              baz: {type: 'integer', maximum: 2}
            }
          },
          bar: {
            type: 'array',
            items: {type: 'string', maxLength: 3},
            minItems: 1
          }
        },
        additionalProperties: false,
        errorMessage: {
          properties: {
            foo: 'data.foo should be an object with the integer property "baz" <= 2, "baz" is ${/foo/baz}',
            bar: 'data.bar should be an array with at least one string item with length <= 3, "bar" is ${/bar}'
          }
        }
      };

      var validData = {
        foo: {baz: 1},
        bar: ['abc']
      };

      ajvs.forEach(function (ajv) {
        validate = ajv.compile(schema);

        assert.strictEqual(validate(validData), true);
        testInvalid({},                 ['required', 'required'], tmpl);
        testInvalid({foo: 1},           ['required', ['type']], tmpl);
        testInvalid({foo: 1, bar: 2},   [['type'], ['type']], tmpl);
        testInvalid({foo: {baz: 'a'}},  ['required', ['type']], tmpl);
        testInvalid({foo: {baz: 3}},    ['required', ['maximum']], tmpl);
        testInvalid({foo: {baz: 3}, bar: []},       [['maximum'], ['minItems']], tmpl);
        testInvalid({foo: {baz: 3}, bar: [1]},      [['maximum'], ['type']], tmpl);
        testInvalid({foo: {baz: 3}, bar: ['abcd']}, [['maximum'], ['maxLength']], tmpl);
      });

      function tmpl(str, data) {
        return str.replace('${/foo/baz}', JSON.stringify(data.foo && data.foo.baz))
                  .replace('${/bar}', JSON.stringify(data.bar));

      }
    });

    it('should replace errors for items with interpolated error messages', function() {
      schema = {
        type: 'array',
        items: [
          {
            type: 'object',
            required: ['baz'],
            properties: {
              baz: {type: 'integer', maximum: 2}
            }
          },
          {
            type: 'array',
            items: {type: 'string', maxLength: 3},
            minItems: 1
          }
        ],
        minItems: 2,
        additionalItems: false,
        errorMessage: {
          items: [
            'data[0] should be an object with the integer property "baz" <= 2, data[0].baz is ${/0/baz}',
            'data[1] should be an array with at least one string item with length <= 3, data[1] is ${/1}'
          ]
        }
      };

      var validData = [
        {baz: 1},
        ['abc']
      ];

      ajvs.forEach(function (ajv) {
        validate = ajv.compile(schema);

        assert.strictEqual(validate(validData), true);
        testInvalid([],            ['minItems'], tmpl);
        testInvalid([1],           ['minItems', ['type']], tmpl);
        testInvalid([1, 2],        [['type'], ['type']], tmpl);
        testInvalid([{baz: 'a'}],  ['minItems', ['type']], tmpl);
        testInvalid([{baz: 3}],    ['minItems', ['maximum']], tmpl);
        testInvalid([{baz: 3}, []],       [['maximum'], ['minItems']], tmpl);
        testInvalid([{baz: 3}, [1]],      [['maximum'], ['type']], tmpl);
        testInvalid([{baz: 3}, ['abcd']], [['maximum'], ['maxLength']], tmpl);
      });

      function tmpl(str, data) {
        return str.replace('${/0/baz}', JSON.stringify(data[0] && data[0].baz))
                  .replace('${/1}', JSON.stringify(data[1]));

      }
    });

    function testInvalid(data, expectedErrors, interpolate) {
      assert.strictEqual(validate(data), false);
      assert.strictEqual(validate.errors.length, expectedErrors.length);
      validate.errors.forEach(function (err, i) {
        var expectedErr = expectedErrors[i];
        if (Array.isArray(expectedErr)) { // errorMessage error
          assert.strictEqual(err.keyword, 'errorMessage');
          var child = Array.isArray(data) ? 'items' : 'properties';
          var expectedMessage = schema.errorMessage[child][err.dataPath.slice(1)];
          if (interpolate) expectedMessage = interpolate(expectedMessage, data);
          assert.strictEqual(err.message, expectedMessage);
          assert((Array.isArray(data) ? /^\/(0|1)$/ : /^\/(foo|bar)$/).test(err.dataPath));
          assert.strictEqual(err.schemaPath, '#/errorMessage');
          var replacedKeywords = err.params.errors.map(function (e) {
            return e.keyword;
          });
          assert.deepEqual(replacedKeywords.sort(), expectedErr.sort());
        } else { // original error
          assert.strictEqual(err.keyword, expectedErr);
        }
      });
    }
  });
});
<|endoftext|>


<commit_before>/**
 * A `Session` is the container that which actually stores
 * and manages driver connection instances.
 *
 * @todo (imlucas): :axe: to `mongodb-session-model`.
 */
var config = require('mongoscope-config');
var uuid = require('uuid');
var async = require('async');
var Instance = require('./instance');
var debug = require('debug')('mongodb-scope-server:models:session');

function generateId(fn) {
  process.nextTick(function() {
    fn(null, uuid.v4());
  });
}

var _reapers = {};
/**
 * @todo (imlucas): All accesses on this should be abstracted
 * into `session-store.js` that will be more explicit and
 * documented as it is our last line of seperation btwn `mongodb-scope-server`
 * and the driver.  That is, if we need to throw buckets of sand
 * on a vuln or compat fire, there is 1 place for it.
 */
var _connections = {};

function destroyReaper(sessionId) {
  if (!_reapers[sessionId]) {
    return debug('No reaper for `%s`', sessionId);
  }

  if (_reapers[sessionId]) {
    clearTimeout(_reapers[sessionId]);
    _reapers[sessionId] = undefined;
    debug('destroyed reaper for `%s`', sessionId);
  }
}

function destroySession(sessionId, fn) {
  debug('destroying `%s`', sessionId);
  if (_connections[sessionId]) {
    _connections[sessionId].close();
    _connections[sessionId] = undefined;
    debug('reaped connection for `%s`', sessionId);
  }
  destroyReaper(sessionId);
  if (fn) {
    return fn();
  }
}

function createReaper(sessionId) {
  debug('creating reaper for `%s`', sessionId);
  /**
   * Reap the session 5 seconds after it is set to
   * expire so the connection to MongoDB will be closed,
   * whether the client is following the rules or not.
   */
  var timeout = config.get('token:lifetime') * 60 * 1000 + 5000;
  var reap = function() {
    /* eslint no-console:0 */
    console.trace('Reaping expired session! ' + sessionId);
    destroySession(sessionId);
  };

  _reapers[sessionId] = setTimeout(reap, timeout);
  debug('created reaper for `%s`.  session will be reaped in %sms', sessionId, timeout);
}

module.exports.create = function(model, fn) {
  debug('creating `%j`', model);
  var tasks = {
    connection: Instance.connect.bind(null, model),
    sessionId: generateId
  };

  async.series(tasks, function(err, res) {
    if (err) {
      return fn(err);
    }

    _connections[res.sessionId] = res.connection;
    createReaper(res.sessionId);

    var session = {
      _id: res.sessionId,
      connection: res.connection
    };
    debug('created! `%s`', session._id);
    fn(null, session);
  });
};

module.exports.destroy = destroySession;

module.exports.get = function(sessionId, fn) {
  debug('get `%j`', sessionId);
  process.nextTick(function() {
    fn(null, _connections[sessionId]);
  });
};

module.exports.exists = function(sessionId, fn) {
  process.nextTick(function() {
    var exists = _connections[sessionId] !== undefined;
    debug('does `%s` already exist?', sessionId, exists);
    fn(null, exists);
  });
};
<commit_msg>:bug: gracefully handle reaping connections already closed<commit_after>/**
 * A `Session` is the container that which actually stores
 * and manages driver connection instances.
 *
 * @todo (imlucas): :axe: to `mongodb-session-model`.
 */
var config = require('mongoscope-config');
var uuid = require('uuid');
var async = require('async');
var Instance = require('./instance');
var debug = require('debug')('mongodb-scope-server:models:session');

function generateId(fn) {
  process.nextTick(function() {
    fn(null, uuid.v4());
  });
}

var _reapers = {};
/**
 * @todo (imlucas): All accesses on this should be abstracted
 * into `session-store.js` that will be more explicit and
 * documented as it is our last line of seperation btwn `mongodb-scope-server`
 * and the driver.  That is, if we need to throw buckets of sand
 * on a vuln or compat fire, there is 1 place for it.
 */
var _connections = {};

function destroyReaper(sessionId) {
  if (!_reapers[sessionId]) {
    return debug('No reaper for `%s`', sessionId);
  }

  if (_reapers[sessionId]) {
    clearTimeout(_reapers[sessionId]);
    _reapers[sessionId] = undefined;
    debug('destroyed reaper for `%s`', sessionId);
  }
}

function destroySession(sessionId, fn) {
  debug('destroying `%s`', sessionId);
  if (_connections[sessionId]) {
    debug('closing driver connection...');
    try {
      _connections[sessionId].close();
    } catch (e) {
      if (e.message === 'This socket is closed.') {
        debug('noop.  driver connection already closed');
      } else {
        throw e;
      }
    }
    _connections[sessionId] = undefined;
    debug('reaped connection for `%s`', sessionId);
  }
  destroyReaper(sessionId);
  if (fn) {
    return fn();
  }
}

function createReaper(sessionId) {
  debug('creating reaper for `%s`', sessionId);
  /**
   * Reap the session 5 seconds after it is set to
   * expire so the connection to MongoDB will be closed,
   * whether the client is following the rules or not.
   */
  var timeout = config.get('token:lifetime') * 60 * 1000 + 5000;
  var reap = function() {
    /* eslint no-console:0 */
    console.trace('Reaping expired session! ' + sessionId);
    destroySession(sessionId);
  };

  _reapers[sessionId] = setTimeout(reap, timeout);
  debug('created reaper for `%s`.  session will be reaped in %sms', sessionId, timeout);
}

module.exports.create = function(model, fn) {
  debug('creating `%j`', model);
  var tasks = {
    connection: Instance.connect.bind(null, model),
    sessionId: generateId
  };

  async.series(tasks, function(err, res) {
    if (err) {
      return fn(err);
    }

    _connections[res.sessionId] = res.connection;
    createReaper(res.sessionId);

    var session = {
      _id: res.sessionId,
      connection: res.connection
    };
    debug('created! `%s`', session._id);
    fn(null, session);
  });
};

module.exports.destroy = destroySession;

module.exports.get = function(sessionId, fn) {
  debug('get `%j`', sessionId);
  process.nextTick(function() {
    fn(null, _connections[sessionId]);
  });
};

module.exports.exists = function(sessionId, fn) {
  process.nextTick(function() {
    var exists = _connections[sessionId] !== undefined;
    debug('does `%s` already exist?', sessionId, exists);
    fn(null, exists);
  });
};
<|endoftext|>


<commit_before>let React = require("react")
let Colr = require('colr')
let {errorList, sizeClassNames, formGroupCx, label} = require("../util.js")
let {div, input} = React.DOM
let cx = require("classnames")

let colorMap = React.createFactory(require("./color/map"))
let hue_slider =  React.createFactory(require("./color/hue_slider"))

export default class extends React.Component {

  static displayName = "Frig.friggingBootstrap.Color"

  static defaultProps = Object.assign(require("../default_props.js"))

  state = { showPopup: false }

  // generate state object from a hex string
  _getStateFrom(color) {
    color = Colr.fromHex(color)
    return {
      color: color,
      hsv: color.toRawHsvObject(),
    }
  }

  _onInputClick() {
    this.setState({
      showPopup: !this.state.showPopup,
    })
  }

  // update the current color using the raw hsv values
  _update() {
    let color = Colr.fromHsvObject(this.state.hsv)
    this.setState({ color: color })
  }

  // set the hue
  _setHue(hue) {
    this.state.hsv.h = hue
    this._update()
  }

  _setHex(hex) {
    hex = hex.replace(/[^\w\s]/gi, '')
    if(hex.length !== 3 && hex.length !== 6) return false

    let color = Colr.fromHex(hex)
    this.setState({
      color: color,
      hsv: color.toRawHsvObject(),
    })
  }

  // set the saturation
  _setSaturation(saturation) {
    this.state.hsv.s = saturation
    this._update()
  }

  // set the saturation and the value
  _setSaturationAndValue(saturation, value) {
    this.state.hsv.s = saturation
    this.state.hsv.v = value
    this._update()
  }

  _getBackgroundHue() {
    return Colr.fromHsv(this.state.hsv.h, 100, 100).toHex()
  }

  _inputMap() {
    let hue = this._getBackgroundHue()
    let luminosity = this.state.color.toGrayscale()

    return colorMap({
      x: this.state.hsv.s,
      y: this.state.hsv.v,
      max: 100,
      backgroundColor: hue,
      className: cx({
        dark: luminosity <= 128,
        light: luminosity > 128,
      }),
      onChange: this._setSaturationAndValue.bind(this),
    })
  }

  _displayColor(){
    if (this.state.showPopup === false) return false
    return div({className: "controls colorpicker"},
      div({ className: "hue-slider" },
        hue_slider({
          value: this.state.hsv.h,
          max: 360,
          onChange: this._setHue.bind(this),
        })
      ),
      this._inputMap(),
    )
  }

  _getCurrentHex() {
    return Colr.fromHsvObject(this.state.hsv).toHex()
  }

  _getContrastYIQ(hexcolor){
    var r = parseInt(hexcolor.substr(1,2),16)
    var g = parseInt(hexcolor.substr(3,2),16)
    var b = parseInt(hexcolor.substr(5,2),16)
    var yiq = ((r*299)+(g*587)+(b*114))/1000

    return (yiq >= 128) ? 'black' : 'white'
  }

  render() {
    return div({className: cx(sizeClassNames(this.props))},
      div({className: formGroupCx(this.props)},
        label(this.props),
        input(Object.assign({}, this.props.inputHtml, {
            valueLink: {
              value: this._getCurrentHex(),
              requestChange: this._setHex.bind(this),
            },
            className: cx(this.props.inputHtml.className, "form-control"),
            onClick: this._onInputClick.bind(this),
            style: {
               backgroundColor: this._getCurrentHex(),
               color: this._getContrastYIQ(this._getCurrentHex()),
            },
          }),
        ),
        this._displayColor(),
        errorList(this.props.errors),
      )
    )
  }

}
<commit_msg>Connect Color Components To Frig valueLink<commit_after>let React = require("react")
let Colr = require('colr')
let {errorList, sizeClassNames, formGroupCx, label} = require("../util.js")
let {div, input} = React.DOM
let cx = require("classnames")

let colorMap = React.createFactory(require("./color/map"))
let hue_slider =  React.createFactory(require("./color/hue_slider"))

export default class extends React.Component {

  static displayName = "Frig.friggingBootstrap.Color"

  static defaultProps = Object.assign(require("../default_props.js"))

  state = { showPopup: false }

  // generate state object from a hex string
  _getStateFrom(color) {
    color = Colr.fromHex(color)
    return {
      color: color,
      hsv: color.toRawHsvObject(),
    }
  }

  _onInputClick() {
    this.setState({
      showPopup: !this.state.showPopup,
    })
  }

  // update the current color using the raw hsv values
  _update() {
    let color = Colr.fromHsvObject(this.state.hsv)
    this.setState({ color: color })
  }

  // set the hue
  _setHue(hue) {
    this.state.hsv.h = hue
    this._update()
  }

  _setHex(hex) {
    hex = hex.replace(/[^\w\s]/gi, '')
    if(hex.length !== 3 && hex.length !== 6) return false

    let color = Colr.fromHex(hex)
    this.setState({
      color: color,
      hsv: color.toRawHsvObject(),
    })
  }

  // set the saturation
  _setSaturation(saturation) {
    this.state.hsv.s = saturation
    this._update()
  }

  // set the saturation and the value
  _setSaturationAndValue(saturation, value) {
    this.state.hsv.s = saturation
    this.state.hsv.v = value
    this._update()
  }

  _getBackgroundHue() {
    return Colr.fromHsv(this.state.hsv.h, 100, 100).toHex()
  }

  _inputMap() {
    let hue = this._getBackgroundHue()
    let luminosity = this.state.color.toGrayscale()

    return colorMap({
      x: this.state.hsv.s,
      y: this.state.hsv.v,
      max: 100,
      backgroundColor: hue,
      className: cx({
        dark: luminosity <= 128,
        light: luminosity > 128,
      }),
      valueLink: this.props.valueLink,
    })
  }

  _displayColor(){
    if (this.state.showPopup === false) return false
    return div({className: "controls colorpicker"},
      div({ className: "hue-slider" },
        hue_slider({
          max: 360,
          valueLink: this.props.valueLink,
        })
      ),
      this._inputMap(),
    )
  }

  _getCurrentHex() {
    return Colr.fromHsvObject(this.state.hsv).toHex()
  }

  _getContrastYIQ(hexcolor){
    var r = parseInt(hexcolor.substr(1,2),16)
    var g = parseInt(hexcolor.substr(3,2),16)
    var b = parseInt(hexcolor.substr(5,2),16)
    var yiq = ((r*299)+(g*587)+(b*114))/1000

    return (yiq >= 128) ? 'black' : 'white'
  }

  render() {
    return div({className: cx(sizeClassNames(this.props))},
      div({className: formGroupCx(this.props)},
        label(this.props),
        input(Object.assign({}, this.props.inputHtml, {
            valueLink: this.props.valueLink,
            className: cx(this.props.inputHtml.className, "form-control"),
            onClick: this._onInputClick.bind(this),
            style: {
               backgroundColor: this._getCurrentHex(),
               color: this._getContrastYIQ(this._getCurrentHex()),
            },
          }),
        ),
        this._displayColor(),
        errorList(this.props.errors),
      )
    )
  }

}
<|endoftext|>


<commit_before>var _ = require('lodash-node');
var fs = require('fs');
var globs = require('glob-stream');
var inquirer = require('inquirer');
var path = require('path');
var Q = require('q');
var exec = require('child_process').exec;
var gulp = require('gulp');
var replace = require('gulp-replace');
var rename = require('gulp-rename');
var through = require('through2');
var rename = require('gulp-rename');
var replace = require('gulp-replace');
var minimist = require('minimist');
var stringify = require('json-stable-stringify');
var paths = require('../config.js').paths;
/**
 *
 * @param {array} questions - objects containing questions to ask in prompt
 * @param {...object} questionOverride - overrides questions in questions array
 */
function inquire(questions){
	var deferred = Q.defer(),
		overrides;
	if(!questions || !questions.length){
		deferred.reject('no questions asked');
	}
	overrides = [].slice.call(arguments, 1);
	questions  = !overrides.length ? questions : skipQuestions(questions, overrides.map(function (override) {
		return override.name;
	}));
	inquirer.prompt(questions, function promptCallback(answers) {
		deferred.resolve(replaceAnswers.apply(null, [answers].concat(overrides)));
	});
	return deferred.promise;
}
/**
 * removes questions
 * @param {array} questions - original array of questions
 * @param {...string|array} names properties of questions to remove from questions array
 */
function skipQuestions(questions) {
	var filters,
		rest = [].slice.call(arguments, 1),
		firstArg = rest.slice().shift();

	filters = Array.isArray(firstArg) ? firstArg : rest;
	return questions.filter(function (question) {
		return filters.indexOf(question.name) < 0;
	});
}
/**
 * replaces answers in given object with replacements
 * @param {object} answers
 * @param {...object} replacements
 *
 */
function replaceAnswers(answers) {
	if(Array.isArray(answers) || typeof answers !== 'object'){
		throw new Error('`answers` argument should be an Object');
	}
	var replacements,
		rest = [].slice.call(arguments, 1),
		firstArg = rest.slice().shift();

	replacements =  Array.isArray(firstArg) ? firstArg : rest;
	if(!replacements.length){
		return answers;
	}
	return _.merge(answers, replacements.reduce(function (prev,curr) {
		var next = {};
		var nextName = curr.name;
		if(nextName) {
			next[nextName] = curr.value;
		}
		return _.assign(prev, next);
	},{}));
}
/**
 * checks if then given module exists and returns its path and an array of file names it contains if
 * it does exists, false if it doesn't
 * @param {string} path
 * @returns {object|boolean}
 */
function getModuleStatus(path){
	return Q.nfcall(fs.readdir, path).then(function (result) {
		return {
			dir:path,
			files:result
		};
	},function () {
		return false;
	});
}
/**
 * returns the file types that do not exist in a module
 *
 * @param {array} fileTypes - files the user requested
 * @param {array} fileNames - base paths that already exist in the module
 * @returns {array} - file extensions the user requested which were not found in the base paths
 */
function getMissingFileTypes(fileTypes, fileNames) {
	if(!Array.isArray(fileTypes)){
		throw new Error('`fileTypes` argument should be an Array');
	}else if(!Array.isArray(fileNames)){
		throw new Error('`moduleStatus` argument should be an Object');
	}
	return addTestFile(fileTypes).filter(function (type) {
		return fileNames.map(function (file) {
				var extension;
				if(!/test/g.test(file)){
					extension = path.extname(file);
				}else{
					extension = '.test.js';
				}
			return extension.substr(1);
		}).indexOf(type) < 0;
	});
}
/**
 * glob search skips '.test.js', so insert the js test file extension here if a js file is found.
 * @param {array} fileTypes
 * @returns {array}
 */
function addTestFile(fileTypes) {
	var adjustedTypes;
	if(fileTypes.indexOf('js') >= 0){
		adjustedTypes = fileTypes.slice();
		adjustedTypes.push('test.js');
		return adjustedTypes;
	}
	return fileTypes;
}
/**
 * returns a module's normalized absolute path
 *
 * @param {String} moduleName
 * @param {String} moduleType
 * @returns {String}
 */
function resolveModuleDir(moduleType, moduleName) {
	return path.resolve(
		path.join(paths.src, moduleType, moduleName)
	);
}
/**
 * create a directory at given path
 * @param {string} path
 * @returns {object} a promise representing the result of directory creation
 */
function createModuleDir(path){
	return Q.nfcall(fs.exists, path).then(function () {
		return Q.nfcall(fs.mkdir, path);
	},function (err) {
		throw new Error('directory already exists!');
	});
}
/**
 *
 * @param {object} module - information about the module to create
 * @returns {*}
 */
function copyTemplateFiles(module) {
	//remove the 's' for a friendly type description in the output files
	var moduleType = module.type.substr(0, module.type.length - 1);
	return gulp.src(module.files.map(function (type) {
		return paths.templateRoot + '/*.' + type;
	}))
		.pipe(replace(/MODULE_NAME/g, module.name))
		.pipe(replace(/MODULE_TYPE/g, moduleType))
		.pipe(rename(function(p){
			var isTest = /test$/.test(p.basename);
			if(p.basename !== 'README' && !isTest){ p.basename = module.name; }
			if(isTest){
				p.basename = module.name;
				p.extname = '.test' + p.extname;
			}
		}))
		.pipe(gulp.dest(module.path));
}
/**
 * prompt the user for module creation.
 * @param {...object} object(s) representing questions to skip / answers to replace
 * todo: get args (use minimist)
 */
function createModule() {
	//ask what to create
	inquire(questions()).then(function (answers) {
		var module = {
			name:	answers.moduleName,
			type:	answers.moduleType,
			files:	answers.files,
			path:	resolveModuleDir(answers.moduleType, answers.moduleName)
		};
		//check if the module already exists
		return getModuleStatus(module.path).then(function (status) {
			//the module already exists, create files that don't exist yet
			if(status){
				module.files = getMissingFileTypes(module.files, status.files)
				return module;
			}
			//the module does not exist yet: create a directory for it
			return createModuleDir(module.path).then(function () {
				return module;
			});
		}).then(function (module) {
			//copy files from template directory to new module
			copyTemplateFiles(module);
			if(module.files.indexOf('js') >= 0){
				registerAmdModule(module.type, module.name);
			}
		});
	}).catch(function (e) {
		console.log(e);
	});
}
function editModule() {

}
function listDirectories(cwd) {
	return Q.nfcall(fs.readdir, cwd)
		.then(function (dirs) {
				return dirs.filter(function(file){
					file = cwd + file;
					return fs.statSync(file).isDirectory();
				});
		});
}
function getModuleIndex() {
	return Q.all([
		listDirectories(paths.srcComponents),
		listDirectories(paths.srcViews)
	]).spread(function (components, views) {
		return {
			components: components.map(function (name) {
				return {
					id: 'components/' + name,
					name: name,
					path: 'components/' + name + '/' + name + '-preview.html',
					type: 'component'
				};
			}),
			views: views.map(function (name) {
				return {
					id: 'views/' + name,
					name: name,
					path: 'views/' + name + '/' + name + '.html',
					type: 'view'
				};
			})
		};
	});
}
/**
 * Create a component or a view with files depending on user feedback through inquirer.
 * if the view or components includes JS, its mapping is added to AMD config.
 * https://www.npmjs.org/package/inquirer
 */
function createModulePrompt(){
	inquirer.prompt(questions(), function createModule(answers) { // callback to inquirer.prompt.
		var moduleType = answers.moduleType;
		var moduleName = answers.moduleName;
		var moduleDir  = [moduleType, moduleName].join('s/');

		console.log(moduleType, moduleName, moduleDir);
		gulp.src(answers.files.map(function (extName) {
			return [paths.src, moduleType + 's/', '_template.', extName].join('');
		}))
			.pipe(replace(/MODULE_NAME/g, moduleName))
			.pipe(rename(function(p){
				if(p.basename !== 'README'){ p.basename = moduleName; }
			}))
			.pipe(gulp.dest(modulePath));

		if(answers.files.indexOf('js') >= 0){
			registerAmdModule(moduleDir, moduleName);
		}
		console.log(['Successfully created', moduleName, moduleType].join(' '));
	});
}
/**
 *  Adds a path to amd-config.json for a convenient alias to the module.
 */
function registerAmdModule(moduleType, moduleName){
	var config = require('../' + paths.amdConfig),
		alias = path.join(moduleType, moduleName),
		location = path.join(alias, moduleName);

	config.paths[alias] = location;
	fs.writeFileSync(path.resolve(paths.amdConfig), stringify(config, {space: 4}));
}
var questions = function () {
	var moduleType, moduleName, modulePath;
	return [{
		type: 'list',
		name: 'moduleType',
		message: 'What kind of module would you like to create?',
		choices:[{
			name: 'component',
			value: 'components'
		},{
			name:'view',
			value: 'views'
		}]
	},{
		type: 'input',
		name: 'moduleName',
		message: 'Give the new module a name',
		validate: function validateModuleName(moduleName) {
			var validName = /^[a-z][a-z0-9-_]+$/.test(moduleName);
			modulePath  = paths.src + moduleType + 's/' + moduleName;
			var validPath = !fs.existsSync(path.normalize(modulePath));
			if(!validName){
				return ['bad', moduleType, 'name'].join(' ');
			}else if(!validPath){
				return ['the', moduleType, 'already exists'].join(' ');
			}
			return true;
		}
	},{
		type:'checkbox',
		name:'files',
		message:'Which types of files do you want to include? Press enter when ready.',
		choices:[
			{ name: 'HTML', value: 'html', checked: true },
			{ name: 'LESS/CSS', value: 'less', checked: true },
			{ name: 'JavaScript', value: 'js', checked: false },
			{ name: 'README', value: 'md', checked: true }
		],
		validate: function(input){
			return (input.length) ? true : 'You must select at least one type of file';
		}
	}];
};

createModule();

module.exports = {
	create: createModule,
	remove: function () { console.log('todo: write remove component task'); },
	edit: editModule,
	//these methods are exposed for unit testing purposes
	skipQuestions: skipQuestions,
	replaceAnswers: replaceAnswers,
	getModuleIndex: getModuleIndex,
	listDirectories: listDirectories,
	getMissingFileTypes: getMissingFileTypes
}<commit_msg>chore(setup): revert getModuleIndex and its dependency to their synchronous version. add first question for edit/remove module tasks.<commit_after>var _ = require('lodash-node');
var fs = require('fs');
var globs = require('glob-stream');
var inquirer = require('inquirer');
var path = require('path');
var Q = require('q');
var exec = require('child_process').exec;
var gulp = require('gulp');
var replace = require('gulp-replace');
var rename = require('gulp-rename');
var through = require('through2');
var rename = require('gulp-rename');
var replace = require('gulp-replace');
var minimist = require('minimist');
var stringify = require('json-stable-stringify');
var paths = require('../config.js').paths;
/**
 *
 * @param {array} questions - objects containing questions to ask in prompt
 * @param {...object} questionOverride - overrides questions in questions array
 */
function inquire(questions){
	var deferred = Q.defer(),
		overrides;
	if(!questions || !questions.length){
		deferred.reject('no questions asked');
	}
	overrides = [].slice.call(arguments, 1);
	questions  = !overrides.length ? questions : skipQuestions(questions, overrides.map(function (override) {
		return override.name;
	}));
	inquirer.prompt(questions, function promptCallback(answers) {
		deferred.resolve(replaceAnswers.apply(null, [answers].concat(overrides)));
	});
	return deferred.promise;
}
/**
 * removes questions
 * @param {array} questions - original array of questions
 * @param {...string|array} names properties of questions to remove from questions array
 */
function skipQuestions(questions) {
	var filters,
		rest = [].slice.call(arguments, 1),
		firstArg = rest.slice().shift();

	filters = Array.isArray(firstArg) ? firstArg : rest;
	return questions.filter(function (question) {
		return filters.indexOf(question.name) < 0;
	});
}
/**
 * replaces answers in given object with replacements
 * @param {object} answers
 * @param {...object} replacements
 *
 */
function replaceAnswers(answers) {
	if(Array.isArray(answers) || typeof answers !== 'object'){
		throw new Error('`answers` argument should be an Object');
	}
	var replacements,
		rest = [].slice.call(arguments, 1),
		firstArg = rest.slice().shift();

	replacements =  Array.isArray(firstArg) ? firstArg : rest;
	if(!replacements.length){
		return answers;
	}
	return _.merge(answers, replacements.reduce(function (prev,curr) {
		var next = {};
		var nextName = curr.name;
		if(nextName) {
			next[nextName] = curr.value;
		}
		return _.assign(prev, next);
	},{}));
}
/**
 * checks if then given module exists and returns its path and an array of file names it contains if
 * it does exists, false if it doesn't
 * @param {string} path
 * @returns {object|boolean}
 */
function getModuleStatus(path){
	return Q.nfcall(fs.readdir, path).then(function (result) {
		return {
			dir:path,
			files:result
		};
	},function () {
		return false;
	});
}
/**
 * returns the file types that do not exist in a module
 *
 * @param {array} fileTypes - files the user requested
 * @param {array} fileNames - base paths that already exist in the module
 * @returns {array} - file extensions the user requested which were not found in the base paths
 */
function getMissingFileTypes(fileTypes, fileNames) {
	if(!Array.isArray(fileTypes)){
		throw new Error('`fileTypes` argument should be an Array');
	}else if(!Array.isArray(fileNames)){
		throw new Error('`moduleStatus` argument should be an Object');
	}
	return addTestFile(fileTypes).filter(function (type) {
		return fileNames.map(function (file) {
				var extension;
				if(!/test/g.test(file)){
					extension = path.extname(file);
				}else{
					extension = '.test.js';
				}
			return extension.substr(1);
		}).indexOf(type) < 0;
	});
}
/**
 * glob search skips '.test.js', so insert the js test file extension here if a js file is found.
 * @param {array} fileTypes
 * @returns {array}
 */
function addTestFile(fileTypes) {
	var adjustedTypes;
	if(fileTypes.indexOf('js') >= 0){
		adjustedTypes = fileTypes.slice();
		adjustedTypes.push('test.js');
		return adjustedTypes;
	}
	return fileTypes;
}
/**
 * returns a module's normalized absolute path
 *
 * @param {String} moduleName
 * @param {String} moduleType
 * @returns {String}
 */
function resolveModuleDir(moduleType, moduleName) {
	return path.resolve(
		path.join(paths.src, moduleType, moduleName)
	);
}
/**
 * create a directory at given path
 * @param {string} path
 * @returns {object} a promise representing the result of directory creation
 */
function createModuleDir(path){
	return Q.nfcall(fs.exists, path).then(function () {
		return Q.nfcall(fs.mkdir, path);
	},function (err) {
		throw new Error('directory already exists!');
	});
}
/**
 *
 * @param {object} module - information about the module to create
 * @returns {*}
 */
function copyTemplateFiles(module) {
	//remove the 's' for a friendly type description in the output files
	var moduleType = module.type.substr(0, module.type.length - 1);
	return gulp.src(module.files.map(function (type) {
		return paths.templateRoot + '/*.' + type;
	}))
		.pipe(replace(/MODULE_NAME/g, module.name))
		.pipe(replace(/MODULE_TYPE/g, moduleType))
		.pipe(rename(function(p){
			var isTest = /test$/.test(p.basename);
			if(p.basename !== 'README' && !isTest){ p.basename = module.name; }
			if(isTest){
				p.basename = module.name;
				p.extname = '.test' + p.extname;
			}
		}))
		.pipe(gulp.dest(module.path));
}
/**
 * prompt the user for module creation.
 * @param {...object} object(s) representing questions to skip / answers to replace
 * todo: get args (use minimist)
 */
function createModule() {
	//ask what to create
	inquire(questions()).then(function (answers) {
		var module = {
			name:	answers.moduleName,
			type:	answers.moduleType,
			files:	answers.files,
			path:	resolveModuleDir(answers.moduleType, answers.moduleName)
		};
		//check if the module already exists
		return getModuleStatus(module.path).then(function (status) {
			//the module already exists, create files that don't exist yet
			if(status){
				module.files = getMissingFileTypes(module.files, status.files)
				return module;
			}
			//the module does not exist yet: create a directory for it
			return createModuleDir(module.path).then(function () {
				return module;
			});
		}).then(function (module) {
			//copy files from template directory to new module
			copyTemplateFiles(module);
			if(module.files.indexOf('js') >= 0){
				registerAmdModule(module.type, module.name);
			}
		});
	}).catch(function (e) {
		console.log(e);
	});
}
function editModule() {

}
function listDirectories(cwd) {
	return fs.readdirSync(cwd)
		.filter(function(file){
			return fs.statSync(cwd + file).isDirectory();
		});
}
function getModuleIndex() {
	return {
		components: listDirectories(paths.srcComponents).map(function(name){
			return {
				id: 'components/' + name,
				name: name,
				path: 'components/' + name + '/' + name + '-preview.html',
				type: 'component'
			};
		}),
		views: listDirectories(paths.srcViews).map(function(name){
			return {
				id: 'views/' + name,
				name: name,
				path: 'views/' + name + '/' + name + '.html',
				type: 'view'
			};
		})
	};
}
/**
 * Create a component or a view with files depending on user feedback through inquirer.
 * if the view or components includes JS, its mapping is added to AMD config.
 * https://www.npmjs.org/package/inquirer
 */
function createModulePrompt(){
	inquirer.prompt(questions(), function createModule(answers) { // callback to inquirer.prompt.
		var moduleType = answers.moduleType;
		var moduleName = answers.moduleName;
		var moduleDir  = [moduleType, moduleName].join('s/');

		console.log(moduleType, moduleName, moduleDir);
		gulp.src(answers.files.map(function (extName) {
			return [paths.src, moduleType + 's/', '_template.', extName].join('');
		}))
			.pipe(replace(/MODULE_NAME/g, moduleName))
			.pipe(rename(function(p){
				if(p.basename !== 'README'){ p.basename = moduleName; }
			}))
			.pipe(gulp.dest(modulePath));

		if(answers.files.indexOf('js') >= 0){
			registerAmdModule(moduleDir, moduleName);
		}
		console.log(['Successfully created', moduleName, moduleType].join(' '));
	});
}
/**
 *  Adds a path to amd-config.json for a convenient alias to the module.
 */
function registerAmdModule(moduleType, moduleName){
	var config = require('../' + paths.amdConfig),
		alias = path.join(moduleType, moduleName),
		location = path.join(alias, moduleName);

	config.paths[alias] = location;
	fs.writeFileSync(path.resolve(paths.amdConfig), stringify(config, {space: 4}));
}
var questions = function () {
	var moduleType, moduleName, modulePath;
	return [{
		type: 'list',
		name: 'moduleType',
		message: 'What kind of module would you like to create?',
		choices:[{
			name: 'component',
			value: 'components'
		},{
			name:'view',
			value: 'views'
		}]
	},{
		type: 'input',
		name: 'moduleName',
		message: 'Give the new module a name',
		validate: function validateModuleName(moduleName) {
			var validName = /^[a-z][a-z0-9-_]+$/.test(moduleName);
			modulePath  = paths.src + moduleType + 's/' + moduleName;
			var validPath = !fs.existsSync(path.normalize(modulePath));
			if(!validName){
				return ['bad', moduleType, 'name'].join(' ');
			}else if(!validPath){
				return ['the', moduleType, 'already exists'].join(' ');
			}
			return true;
		}
	},{
		type:'checkbox',
		name:'files',
		message:'Which types of files do you want to include? Press enter when ready.',
		choices:[
			{ name: 'HTML', value: 'html', checked: true },
			{ name: 'LESS/CSS', value: 'less', checked: true },
			{ name: 'JavaScript', value: 'js', checked: false },
			{ name: 'README', value: 'md', checked: true }
		],
		validate: function(input){
			return (input.length) ? true : 'You must select at least one type of file';
		}
	}];
};
var editQuestions = function () {
	//question 1: select from a list of existing modules
	var questions = [
		{
			type: 'list',
			name: 'path',
			message: 'select a component to add to',
			choices:function () {
				var index = getConsoleModuleIndex(getModuleIndex());
				return [new inquirer.Separator('=== COMPONENTS:')].concat(
					index.components
				).concat([new inquirer.Separator('=== VIEWS:')]).concat(
					index.views
				);
			}
		}
	];
	return questions;
};

function getConsoleModuleIndex(index) {
	return Object.keys(index).map(function (type) {
		var section = {};
		section[type] = index[type].map(function (module) {
			return {
				name: module.name,
				value: module.id
			};
		});
		return section;
	}).reduce(function (prev,curr) {
		var keys = Object.keys(curr);
		prev[ keys[0] ] = curr[ keys[0] ];
		return prev;
	},{});
}

module.exports = {
	create: createModule,
	remove: function () { console.log('todo: write remove component task'); },
	edit: editModule,
	//these methods are exposed for unit testing purposes
	skipQuestions: skipQuestions,
	replaceAnswers: replaceAnswers,
	getModuleIndex: getModuleIndex,
	listDirectories: listDirectories,
	getMissingFileTypes: getMissingFileTypes
}<|endoftext|>


<commit_before>import {UICorePlugin, Events, Styler, template} from 'Clappr'

import pluginStyle from './public/context_menu.scss'
import templateHtml from './public/context_menu.html'

export default class ContextMenuPlugin extends UICorePlugin {
  get name() { return 'context_menu' }
  get attributes() { return { 'class': 'context-menu' } }
  get template() { return template(templateHtml) }
  get loopEnable() { return this.core.activePlayback.el.loop }

  get exposeVersion() {
    return {
      label: `Clappr Player v${Clappr.version}`,
      name: 'version'
    }
  }

  get copyURL() {
    return {
      label: 'Copy URL',
      name: 'copyURL'
    }
  }

  get copyURLCurrentTime() {
    return {
      label: 'Copy URL on current time',
      name: 'copyURLCurrentTime'
    }
  }

  get loop() {
    return {
      label: 'Loop: ',
      name: 'loop',
      class: this.core.options.loop ? 'on' : 'off'
    }
  }

  get events() {
    let events = {
      'click [data-copyURL]': 'onCopyURL',
      'click [data-copyURLCurrentTime]': 'onCopyURLCurrentTime',
      'click [data-loop]': 'onToggleLoop'
    }
    this.extraOptions && this.extraOptions.forEach((item) => {
      if (typeof item.callback === 'function') {
        let callbackName = `${item.name}Callback`
        this[callbackName] = item.callback
        events[`click [data-${item.name}]`] = callbackName
      }
    })
    return events
  }

  constructor(core) {
    super(core)
    this.extraOptions = this.options.contextMenu && this.options.contextMenu.extraOptions || []
    this.delegateEvents(this.events)
    this.bindEvents()
  }

  bindEvents() {
    const coreEventListenerData = [
      { object: this.core, event: Events.CORE_ACTIVE_CONTAINER_CHANGED, callback: this.containerChanged },
    ]

    this.stopListening(coreEventListenerData[0].object, coreEventListenerData[0].event, coreEventListenerData[0].callback)
    this.listenTo(coreEventListenerData[0].object, coreEventListenerData[0].event, coreEventListenerData[0].callback)

    this.bindCustomEvents()
  }

  bindContainerEvents() {
    const containerEventListenerData = [
      { object: this.container, event: Events.CONTAINER_CONTEXTMENU, callback: this.toggleContextMenu },
      { object: this.container, event: Events.CONTAINER_CLICK, callback: this.hide },
    ]

    if (this.container) {
      containerEventListenerData.forEach(item => this.stopListening(item.object, item.event, item.callback))
      containerEventListenerData.forEach(item => this.listenTo(item.object, item.event, item.callback))
    }
  }

  bindCustomEvents() {
    $('body').off('click', this.hide.bind(this))
    $('body').on('click', this.hide.bind(this))
  }

  destroy() {
    $('body').off('click', this.hide.bind(this))
    this.stopListening()
    super.destroy()
  }

  containerChanged() {
    this.container = this.core.activeContainer
    this.bindContainerEvents()
  }

  toggleContextMenu(event) {
    event.preventDefault()
    this.show(event.offsetY, event.offsetX)
  }

  show(top, left) {
    !this.playerElement && this.calculateContextMenuLimit()
    let finalTop = top > this.maxHeight ? this.maxHeight : top
    let finalLeft = left > this.maxWidth ? this.maxWidth : left
    this.hide()
    this.$el.css({ top: finalTop, left: finalLeft})
    this.$el.show()
  }

  calculateContextMenuLimit() {
    this.playerElement = document.querySelector('[data-player]')
    this.maxWidth = this.playerElement.clientWidth - 160
    this.maxHeight = this.playerElement.clientHeight - 200
  }

  hide() {
    this.$el.hide()
  }

  copyToClipboard(value, $el) {
    if (!$el) return

    let $copyTextarea = $('<textarea class="copytextarea"/>')
    $copyTextarea.text(value)
    $el.append($copyTextarea[0])

    let copyTextarea = document.querySelector('.context-menu .copytextarea')
    copyTextarea.select()

    try {
      document.execCommand('copy')
    } catch (err) {
      throw Error(err)
    }

    $copyTextarea.remove()
  }

  onCopyURL() {
    this.copyToClipboard(window.location.href, this.$el)
  }

  onCopyURLCurrentTime() {
    let url = window.location.href
    const currentTime = Math.floor(this.container.getCurrentTime())

    if (window.location.search === '') { //if dont exist any query string
      url += `?t=${currentTime}`
    } else if (window.location.search.split(/[\?=&]/g).indexOf('t') === -1) { // if exist query string but not the resume at
      url += `&t=${currentTime}`
    } else if (window.location.search.split(/[\?=&]/g).indexOf('t') !== -1) { // if exist resume query string
      let search = window.location.search.split(/[\?&]/g)
      let resumeAtQueryString = search.find(item => item.includes('t='))
      let newQueryString = window.location.search.replace(resumeAtQueryString, `t=${currentTime}`)
      url = `${url.replace(window.location.search, '')}${newQueryString}`
    }

    this.copyToClipboard(url, this.$el)
  }

  onToggleLoop() {
    this.core.options.loop = !this.loopEnable
    this.core.activePlayback.el.loop = !this.loopEnable
    this.$el.find('[data-loop]').toggleClass('on', this.loopEnable)
    this.$el.find('[data-loop]').toggleClass('off', !this.loopEnable)
  }

  appendExtraOptions(item) {
    this.menuOptions.unshift(item)
  }

  addCustomStyle() {
    const styles = this.options.contextMenu && this.options.contextMenu.customStyle
    if (styles) {
      this.$el.css(styles.container)
      this.$el.find('.context-menu-list').css(styles.list)
      this.$el.find('.context-menu-list-item').css(styles.items)
    }
  }

  render() {
    this.menuOptions = [this.copyURL, this.copyURLCurrentTime, this.loop, this.exposeVersion]
    this.options.contextMenu && this.options.contextMenu.extraOptions && this.options.contextMenu.extraOptions.forEach(item => this.appendExtraOptions(item))
    this.$el.html(this.template({ options: this.menuOptions }))
    this.$el.append(Styler.getStyleFor(pluginStyle))
    this.core.$el[0].append(this.$el[0])
    this.hide()
    this.disable()
    this.addCustomStyle()
    return this
  }
}
<commit_msg>feat(src): add new config to define initial menu itens<commit_after>import {UICorePlugin, Events, Styler, template} from 'Clappr'

import pluginStyle from './public/context_menu.scss'
import templateHtml from './public/context_menu.html'

export default class ContextMenuPlugin extends UICorePlugin {
  get name() { return 'context_menu' }
  get attributes() { return { 'class': 'context-menu' } }
  get template() { return template(templateHtml) }
  get loopEnable() { return this.core.activePlayback.el.loop }

  get playerVersion() {
    return {
      label: `Clappr Player v${Clappr.version}`,
      name: 'playerVersion'
    }
  }

  get copyURL() {
    return {
      label: 'Copy URL',
      name: 'copyURL'
    }
  }

  get copyURLCurrentTime() {
    return {
      label: 'Copy URL on current time',
      name: 'copyURLCurrentTime'
    }
  }

  get loop() {
    return {
      label: 'Loop: ',
      name: 'loop',
      class: this.core.options.loop ? 'on' : 'off'
    }
  }

  get events() {
    let events = {
      'click [data-copyURL]': 'onCopyURL',
      'click [data-copyURLCurrentTime]': 'onCopyURLCurrentTime',
      'click [data-loop]': 'onToggleLoop'
    }
    this.extraOptions && this.extraOptions.forEach((item) => {
      if (typeof item.callback === 'function') {
        let callbackName = `${item.name}Callback`
        this[callbackName] = item.callback
        events[`click [data-${item.name}]`] = callbackName
      }
    })
    return events
  }

  constructor(core) {
    super(core)
    this.extraOptions = this.options.contextMenu && this.options.contextMenu.extraOptions || []
    this.delegateEvents(this.events)
    this.bindEvents()
  }

  bindEvents() {
    const coreEventListenerData = [
      { object: this.core, event: Events.CORE_ACTIVE_CONTAINER_CHANGED, callback: this.containerChanged },
    ]

    this.stopListening(coreEventListenerData[0].object, coreEventListenerData[0].event, coreEventListenerData[0].callback)
    this.listenTo(coreEventListenerData[0].object, coreEventListenerData[0].event, coreEventListenerData[0].callback)

    this.bindCustomEvents()
  }

  bindContainerEvents() {
    const containerEventListenerData = [
      { object: this.container, event: Events.CONTAINER_CONTEXTMENU, callback: this.toggleContextMenu },
      { object: this.container, event: Events.CONTAINER_CLICK, callback: this.hide },
    ]

    if (this.container) {
      containerEventListenerData.forEach(item => this.stopListening(item.object, item.event, item.callback))
      containerEventListenerData.forEach(item => this.listenTo(item.object, item.event, item.callback))
    }
  }

  bindCustomEvents() {
    $('body').off('click', this.hide.bind(this))
    $('body').on('click', this.hide.bind(this))
  }

  destroy() {
    $('body').off('click', this.hide.bind(this))
    this.stopListening()
    super.destroy()
  }

  containerChanged() {
    this.container = this.core.activeContainer
    this.bindContainerEvents()
  }

  toggleContextMenu(event) {
    event.preventDefault()
    this.show(event.offsetY, event.offsetX)
  }

  show(top, left) {
    !this.playerElement && this.calculateContextMenuLimit()
    let finalTop = top > this.maxHeight ? this.maxHeight : top
    let finalLeft = left > this.maxWidth ? this.maxWidth : left
    this.hide()
    this.$el.css({ top: finalTop, left: finalLeft})
    this.$el.show()
  }

  calculateContextMenuLimit() {
    this.playerElement = document.querySelector('[data-player]')
    this.maxWidth = this.playerElement.clientWidth - 160
    this.maxHeight = this.playerElement.clientHeight - 200
  }

  hide() {
    this.$el.hide()
  }

  copyToClipboard(value, $el) {
    if (!$el) return

    let $copyTextarea = $('<textarea class="copytextarea"/>')
    $copyTextarea.text(value)
    $el.append($copyTextarea[0])

    let copyTextarea = document.querySelector('.context-menu .copytextarea')
    copyTextarea.select()

    try {
      document.execCommand('copy')
    } catch (err) {
      throw Error(err)
    }

    $copyTextarea.remove()
  }

  onCopyURL() {
    this.copyToClipboard(window.location.href, this.$el)
  }

  onCopyURLCurrentTime() {
    let url = window.location.href
    const currentTime = Math.floor(this.container.getCurrentTime())

    if (window.location.search === '') { //if dont exist any query string
      url += `?t=${currentTime}`
    } else if (window.location.search.split(/[\?=&]/g).indexOf('t') === -1) { // if exist query string but not the resume at
      url += `&t=${currentTime}`
    } else if (window.location.search.split(/[\?=&]/g).indexOf('t') !== -1) { // if exist resume query string
      let search = window.location.search.split(/[\?&]/g)
      let resumeAtQueryString = search.find(item => item.includes('t='))
      let newQueryString = window.location.search.replace(resumeAtQueryString, `t=${currentTime}`)
      url = `${url.replace(window.location.search, '')}${newQueryString}`
    }

    this.copyToClipboard(url, this.$el)
  }

  onToggleLoop() {
    this.core.options.loop = !this.loopEnable
    this.core.activePlayback.el.loop = !this.loopEnable
    this.$el.find('[data-loop]').toggleClass('on', this.loopEnable)
    this.$el.find('[data-loop]').toggleClass('off', !this.loopEnable)
  }

  appendExtraOptions(item) {
    this.menuOptions.unshift(item)
  }

  addCustomStyle() {
    const styles = this.options.contextMenu && this.options.contextMenu.customStyle
    if (styles) {
      this.$el.css(styles.container)
      this.$el.find('.context-menu-list').css(styles.list)
      this.$el.find('.context-menu-list-item').css(styles.items)
    }
  }

  sanitizeCustomizedItems() {
    const customMenuItems = []
    this.options.contextMenu.menuItems.forEach(item => {
      typeof this[item] !== 'undefined' && customMenuItems.push(this[item])
    })
    return customMenuItems
  }

  render() {
    this.customMenuItems = this.options.contextMenu && this.options.contextMenu.menuItems && this.sanitizeCustomizedItems()
    this.menuOptions = this.customMenuItems && this.customMenuItems.length > 0 ? this.customMenuItems : [this.copyURL, this.copyURLCurrentTime, this.loop, this.playerVersion]
    this.options.contextMenu && this.options.contextMenu.extraOptions && this.options.contextMenu.extraOptions.forEach(item => this.appendExtraOptions(item))
    this.$el.html(this.template({ options: this.menuOptions }))
    this.$el.append(Styler.getStyleFor(pluginStyle))
    this.core.$el[0].append(this.$el[0])
    this.hide()
    this.disable()
    this.addCustomStyle()
    return this
  }
}
<|endoftext|>


<commit_before>(function() {
	var ua = navigator.userAgent.toLowerCase(),
		ie = !!window.ActiveXObject,
		webkit = ua.indexOf("webkit") != -1,
		mobile = typeof orientation != 'undefined' ? true : false,
		android = ua.indexOf("android") != -1,
		opera = window.opera;

	L.Browser = {
		ie: ie,
		ie6: ie && !window.XMLHttpRequest,

		webkit: webkit,
		webkit3d: webkit && ('WebKitCSSMatrix' in window) && ('m11' in new WebKitCSSMatrix()),

		gecko: ua.indexOf("gecko") != -1,

		opera: opera,

		android: android,
		mobileWebkit: mobile && webkit,
		mobileOpera: mobile && opera,

		mobile: mobile,
		touch: (function() {
			var touchSupported = false;;

			// WebKit, etc
			if ('ontouchstart' in document.documentElement) {
				return true;
			}

			// Firefox/Gecko
			var e = document.createElement('div');

			// If no support for basic event stuff, unlikely to have touch support
			if (!e.setAttribute || !e.removeAttribute) {
				return false;
			}

			e.setAttribute('ontouchstart', 'return;');
			if (typeof e['ontouchstart'] == 'function') {
				touchSupported = true;
			}

			e.removeAttribute('ontouchstart');
			e = null;

			return touchSupported;
		})()
	};
})();
<commit_msg>remove extra semicolon<commit_after>(function() {
	var ua = navigator.userAgent.toLowerCase(),
		ie = !!window.ActiveXObject,
		webkit = ua.indexOf("webkit") != -1,
		mobile = typeof orientation != 'undefined' ? true : false,
		android = ua.indexOf("android") != -1,
		opera = window.opera;

	L.Browser = {
		ie: ie,
		ie6: ie && !window.XMLHttpRequest,

		webkit: webkit,
		webkit3d: webkit && ('WebKitCSSMatrix' in window) && ('m11' in new WebKitCSSMatrix()),

		gecko: ua.indexOf("gecko") != -1,

		opera: opera,

		android: android,
		mobileWebkit: mobile && webkit,
		mobileOpera: mobile && opera,

		mobile: mobile,
		touch: (function() {
			var touchSupported = false;

			// WebKit, etc
			if ('ontouchstart' in document.documentElement) {
				return true;
			}

			// Firefox/Gecko
			var e = document.createElement('div');

			// If no support for basic event stuff, unlikely to have touch support
			if (!e.setAttribute || !e.removeAttribute) {
				return false;
			}

			e.setAttribute('ontouchstart', 'return;');
			if (typeof e['ontouchstart'] == 'function') {
				touchSupported = true;
			}

			e.removeAttribute('ontouchstart');
			e = null;

			return touchSupported;
		})()
	};
})();
<|endoftext|>