<commit_before>var fs = require('fs');
var path = require('path');
var stripe = require('stripe');
var q = require('q');
var request = require('request');
/**
 * A bridge connecting le-bank-service to Stripe
 * @class BankProvider
 * @param {string} secretKey the stripe secret key for your account
 * @param {StorageService} storage an instance of le-storage-service that is used to create records
 * @returns {service}
 */
var BankProvider = function(secretKey, storage) {
  if (!secretKey) {
    throw new Error('Secret key required');
  }
  if (!storage) {
    throw new Error('Storage service required');
  }
  var _provider = this;
  var _api = stripe(secretKey);
  /**
   * Creates a bank account
   *
   * Requires two tokens, one for crediting the bank account
   * and one for debiting the bank account. Simply generate
   * two tokens with the same bank account credentials.
   * @function createBankAccount
   * @memberof BankProvider
   * @instance
   * @param {string} countryCode the two letter country code of the bank's origin
   * @param {string} creditToken the tokenized bank account info
   * @param {string} debitToken the tokenized bank account info
   * @param {string} email the email to assocaite with the stripe customer
   * @returns {promise} resolves with the newly created bankAccount record
   */
  this.createBankAccount = function(countryCode, creditToken, debitToken, email) {
    var bankAccount;
    var promises = [];
    var accountData = {};
    accountData.managed = true;
    accountData.country = countryCode;
    accountData.bank_account = creditToken;
    if (email) {
      accountData.email = email;
    }
    promises.push(_api.accounts.create(accountData));
    var customerData = {};
    customerData.bank_account = debitToken;
    if (email) {
      customerData.email = email;
    }
    promises.push(_api.customers.create(customerData));
    return q.all(promises)
      .spread(function(account, customer) {
        bankAccount = storage.createRecord('Bank Account');
        return bankAccount.update({
          _stripe: {
            customer_id: customer.id,
            account_id: account.id,
            bankAccount_id: customer.default_source
          }
        });
      })
      .then(function() {
        return bankAccount
      });
  };
  /**
   * Creates a credit card
   * @function createCreditCard
   * @memberof BankProvider
   * @instance
   * @param {string} token the tokenized credit card info
   * @param {string} email the email to assocaite with the stripe customer
   * @returns {promise} resolves with the newly created creditCard record
   */
  this.createCreditCard = function(token, email) {
    var customerData = {};
    customerData.card = token;
    if (email) {
      customerData.email = email;
    }
    return _api.customers.create(customerData)
      .then(function(customer) {
        var creditCard = storage.createRecord('Credit Card');
        return creditCard.update({
            _stripe: {
              customer_id: customer.id,
              creditCard_id: customer.default_source
            }
          })
          .then(function() {
            return creditCard;
          });
      });
  };
  /**
   * Verifies that the user has access to the bank account using micro-deposits
   *
   * @function verifyBankAccount
   * @memberof BankProvider
   * @instance
   * @param {record} bankAccount the record of the bank account to be verified
   * @param {array} amounts the micro-deposit verification amounts
   * @returns {promise}
   */
  this.verifyBankAccount = function(bankAccount, amounts) {
    return bankAccount.load()
      .then(function(data) {
        var customer = data._stripe.customer_id;
        var id = data._stripe.bankAccount_id;
        var deferred = q.defer();
        // node client doesn't support verifying bank accounts, so we must make the HTTP request ourselves :/
        var url = 'https://api.stripe.com/v1/customers/' + customer + '/bank_accounts/' + id + '/verify';
        var options = {
          url: url,
          auth: {
            'bearer': secretKey
          },
          form: {
            amounts: amounts
          },
          qsStringifyOptions: {
            arrayFormat: 'brackets'
          }
        };
        request.post(options, function(err, resp) {
          if (!err && resp.statusCode == 200) {
            data.verifiedAt = new Date();
            bankAccount.update(data)
              .then(function() {
                deferred.resolve();
              })
          } else {
            errMessage = JSON.parse(resp.body).error.message;
            deferred.reject(errMessage);
          }
        });
        return deferred.promise;
      });
  };
  /**
   * Verifies that the user is who they say they are
   *
   * Basic verification requires the following properties on the identity object:
   * - legal_entity.first_name
   * - legal_entity.last_name
   * - legal_entity.dob.day
   * - legal_entity.dob.month
   * - legal_entity.dob.year
   * - legal_entity.type
   * - tos_acceptance.ip
   * - tos_acceptance.date
   * @function verifyIdentity
   * @memberof BankProvider
   * @instance
   * @param {record} bankAccount the record of the bank account to be verified
   * @param {Object} identity the map of identity fields
   * @param {string} filepath (optional) path to uploaded image of government ID
   * @returns {promise}
   */
  this.verifyIdentity = function(bankAccount, identity, filepath) {
    var account;
    return bankAccount.load()
      .then(function(data) {
        account = data._stripe.account_id;
        var promise = _api.accounts.update(account, identity);
        if (filepath) {
          var filename = path.basename(filepath);
          promise.then(function() {
            return _api.fileUploads.create({
              purpose: 'identity_document',
              file: {
                data: fs.readFileSync(filepath),
                name: filename,
                type: 'application/octet-stream'
              }
            }, {
              stripe_account: account
            });
          })
            .then(function(file) {
              var fileID = file.id;
              return _api.accounts.update(account, {
                legal_entity: {
                  verification: {
                    document: fileID
                  }
                }
              });
            });
        }
        return promise;
      })
  };
  /**
   * Checks that the verification process was successful
   * @function isIdentityVerified
   * @memberof BankProvider
   * @instance
   * @param {record} bankAccount the record of the bank account to be verified
   * @returns {promise}
   */
  this.isIdentityVerified = function(bankAccount) {
    var account;
    return bankAccount.load()
      .then(function(data) {
        account = data._stripe.account_id;
        return _api.accounts.retrieve(account);
      })
      .then(function(data) {
        if (data &&
          data.legal_entity &&
          data.legal_entity.verification &&
          data.legal_entity.verification.status) {
          var verification = data.legal_entity.verification;
          var status = verification.status;
          if (status === 'verified') {
            return;
          } else if (status === 'pending') {
            return q.reject(new Error('Identity verification pending'));
          } else if (status === 'unverified') {
            if (verification.details) {
              return q.reject(new Error(verification.details));
            } else {
              return q.reject(new Error('Identity unverified, reason unknown'));
            }
          }
        } else {
          return q.reject(new Error('Identity verification status missing from Stripe account'));
        }
      });
  };
  /**
   * Charges a stripe customer
   * @function chargeCustomer
   * @memberof BankProvider
   * @instance
   * @param {string} customer the id of the stripe customer to charge
   * @param {number} cents the numbers of cents to charge
   * @param {string} accountID (optional) the id of the stripe account to credit
   * @param {string} description intended for end users to read, such as in a bank statement
   * @returns {promise}
   */
  this.chargeCustomer = function (customer, cents, accountID, description) {
    if (!description) {
      description = 'Castle';
    }
    var promise;
    if (accountID) {
      var charge;
      promise = _api.charges.create({
        amount: cents,
        currency: 'usd',
        customer: customer,
        expand: ['transfer'],
        destination: accountID,
        description: description
      });
    } else {
      promise = _api.charges.create({
        amount: cents,
        currency: 'usd',
        customer: customer,
        description: description
      });
    }
    return promise;
  };
  /**
   * Charges a bank account
   * @function chargeBankAccount
   * @memberof BankProvider
   * @instance
   * @param {record} bankAccount the record of the bank account to be charged
   * @param {number} cents the numbers of cents to charge
   * @returns {promise} resolves with the newly created payment record
   */
  this.chargeBankAccount = function(bankAccount, cents) {
    var customer;
    var payment;
    return bankAccount.load()
      .then(function(data) {
        if (data.verifiedAt) {
          customer = data._stripe.customer_id;
          return _provider.chargeCustomer(customer, cents);
        } else {
          return q.reject(new Error('Bank accounts must be verified before they can be charged'));
        }
      })
      .then(function(charge) {
        payment = storage.createRecord('Payment');
        return payment.update({
          cents: cents,
          _stripe: {
            customer_id: customer,
            charge_id: charge.id
          }
        });
      })
      .then(function() {
        return payment;
      });
  };
  /**
   * Charges a credit card
   * @function chargeCreditCard
   * @memberof BankProvider
   * @instance
   * @param {record} card the record of the credit card to be charged
   * @param {number} cents the numbers of cents to charge
   * @returns {promise} resolves with the newly created payment record
   */
  this.chargeCreditCard = function(card, cents) {
    var customer;
    var payment;
    return card.load()
      .then(function(data) {
        customer = data._stripe.customer_id;
        return _provider.chargeCustomer(customer, cents);
      })
      .then(function(charge) {
        payment = storage.createRecord('Payment');
        return payment.update({
          cents: cents,
          _stripe: {
            customer_id: customer,
            charge_id: charge.id
          }
        });
      })
      .then(function() {
        return payment;
      });
  };
  /**
   * Transfers money from a funding source to a bank account
   * @function transfer
   * @memberof BankProvider
   * @instance
   * @param {record} source the record of the credit card or bank account to be charged
   * @param {record} destination the record of the bank account to be credited
   * @param {number} cents the number of cents to transfer
   * @param {string} description intended for end users to read, such as in a bank statement
   * @returns {promise} resolves with the newly created payment record
   */
  this.transfer = function(source, destination, cents, description) {
    var customer;
    var accountID;
    var payment;
    return source.load()
      .then(function(data) {
        customer = data._stripe.customer_id;
        return destination.load();
      })
      .then(function(data) {
        accountID = data._stripe.account_id;
        return _provider.chargeCustomer(customer, cents, accountID, description);
      })
      .then(function(charge) {
        payment = storage.createRecord('Payment');
        return payment.update({
          cents: cents,
          _stripe: {
            customer_id: customer,
            account_id: accountID,
            charge_id: charge.id
          }
        });
      })
      .then(function() {
        return payment;
      });
  };
  /**
   * Returns a bank account record, given the id
   * @function getBankAccount
   * @memberof BankProvider
   * @instance
   * @param {string} id the id of the bank account record
   * @returns {record}
   */
  this.getBankAccount = function(id) {
    return storage.createRecord('Bank Account', id);
  }
  /**
   * Returns a credit card record, given the id
   * @function getCreditCard
   * @memberof BankProvider
   * @instance
   * @param {string} id the id of the credit card record
   * @returns {record}
   */
  this.getCreditCard = function(id) {
    return storage.createRecord('Credit Card', id);
  }
};

module.exports = BankProvider;
<commit_msg>feat(index): make new accounts set to manual transfers<commit_after>var fs = require('fs');
var path = require('path');
var stripe = require('stripe');
var q = require('q');
var request = require('request');
/**
 * A bridge connecting le-bank-service to Stripe
 * @class BankProvider
 * @param {string} secretKey the stripe secret key for your account
 * @param {StorageService} storage an instance of le-storage-service that is used to create records
 * @returns {service}
 */
var BankProvider = function(secretKey, storage) {
  if (!secretKey) {
    throw new Error('Secret key required');
  }
  if (!storage) {
    throw new Error('Storage service required');
  }
  var _provider = this;
  var _api = stripe(secretKey);
  /**
   * Creates a bank account
   *
   * Requires two tokens, one for crediting the bank account
   * and one for debiting the bank account. Simply generate
   * two tokens with the same bank account credentials.
   * @function createBankAccount
   * @memberof BankProvider
   * @instance
   * @param {string} countryCode the two letter country code of the bank's origin
   * @param {string} creditToken the tokenized bank account info
   * @param {string} debitToken the tokenized bank account info
   * @param {string} email the email to assocaite with the stripe customer
   * @returns {promise} resolves with the newly created bankAccount record
   */
  this.createBankAccount = function(countryCode, creditToken, debitToken, email) {
    var bankAccount;
    var promises = [];
    var accountData = {};
    accountData.managed = true;
    accountData.country = countryCode;
    accountData.bank_account = creditToken;
    accountData.transfer_schedule = {
        'interval': 'manual'
    };
    if (email) {
      accountData.email = email;
    }
    promises.push(_api.accounts.create(accountData));
    var customerData = {};
    customerData.bank_account = debitToken;
    if (email) {
      customerData.email = email;
    }
    promises.push(_api.customers.create(customerData));
    return q.all(promises)
      .spread(function(account, customer) {
        bankAccount = storage.createRecord('Bank Account');
        return bankAccount.update({
          _stripe: {
            customer_id: customer.id,
            account_id: account.id,
            bankAccount_id: customer.default_source
          }
        });
      })
      .then(function() {
        return bankAccount
      });
  };
  /**
   * Creates a credit card
   * @function createCreditCard
   * @memberof BankProvider
   * @instance
   * @param {string} token the tokenized credit card info
   * @param {string} email the email to assocaite with the stripe customer
   * @returns {promise} resolves with the newly created creditCard record
   */
  this.createCreditCard = function(token, email) {
    var customerData = {};
    customerData.card = token;
    if (email) {
      customerData.email = email;
    }
    return _api.customers.create(customerData)
      .then(function(customer) {
        var creditCard = storage.createRecord('Credit Card');
        return creditCard.update({
            _stripe: {
              customer_id: customer.id,
              creditCard_id: customer.default_source
            }
          })
          .then(function() {
            return creditCard;
          });
      });
  };
  /**
   * Verifies that the user has access to the bank account using micro-deposits
   *
   * @function verifyBankAccount
   * @memberof BankProvider
   * @instance
   * @param {record} bankAccount the record of the bank account to be verified
   * @param {array} amounts the micro-deposit verification amounts
   * @returns {promise}
   */
  this.verifyBankAccount = function(bankAccount, amounts) {
    return bankAccount.load()
      .then(function(data) {
        var customer = data._stripe.customer_id;
        var id = data._stripe.bankAccount_id;
        var deferred = q.defer();
        // node client doesn't support verifying bank accounts, so we must make the HTTP request ourselves :/
        var url = 'https://api.stripe.com/v1/customers/' + customer + '/bank_accounts/' + id + '/verify';
        var options = {
          url: url,
          auth: {
            'bearer': secretKey
          },
          form: {
            amounts: amounts
          },
          qsStringifyOptions: {
            arrayFormat: 'brackets'
          }
        };
        request.post(options, function(err, resp) {
          if (!err && resp.statusCode == 200) {
            data.verifiedAt = new Date();
            bankAccount.update(data)
              .then(function() {
                deferred.resolve();
              })
          } else {
            errMessage = JSON.parse(resp.body).error.message;
            deferred.reject(errMessage);
          }
        });
        return deferred.promise;
      });
  };
  /**
   * Verifies that the user is who they say they are
   *
   * Basic verification requires the following properties on the identity object:
   * - legal_entity.first_name
   * - legal_entity.last_name
   * - legal_entity.dob.day
   * - legal_entity.dob.month
   * - legal_entity.dob.year
   * - legal_entity.type
   * - tos_acceptance.ip
   * - tos_acceptance.date
   * @function verifyIdentity
   * @memberof BankProvider
   * @instance
   * @param {record} bankAccount the record of the bank account to be verified
   * @param {Object} identity the map of identity fields
   * @param {string} filepath (optional) path to uploaded image of government ID
   * @returns {promise}
   */
  this.verifyIdentity = function(bankAccount, identity, filepath) {
    var account;
    return bankAccount.load()
      .then(function(data) {
        account = data._stripe.account_id;
        var promise = _api.accounts.update(account, identity);
        if (filepath) {
          var filename = path.basename(filepath);
          promise.then(function() {
            return _api.fileUploads.create({
              purpose: 'identity_document',
              file: {
                data: fs.readFileSync(filepath),
                name: filename,
                type: 'application/octet-stream'
              }
            }, {
              stripe_account: account
            });
          })
            .then(function(file) {
              var fileID = file.id;
              return _api.accounts.update(account, {
                legal_entity: {
                  verification: {
                    document: fileID
                  }
                }
              });
            });
        }
        return promise;
      })
  };
  /**
   * Checks that the verification process was successful
   * @function isIdentityVerified
   * @memberof BankProvider
   * @instance
   * @param {record} bankAccount the record of the bank account to be verified
   * @returns {promise}
   */
  this.isIdentityVerified = function(bankAccount) {
    var account;
    return bankAccount.load()
      .then(function(data) {
        account = data._stripe.account_id;
        return _api.accounts.retrieve(account);
      })
      .then(function(data) {
        if (data &&
          data.legal_entity &&
          data.legal_entity.verification &&
          data.legal_entity.verification.status) {
          var verification = data.legal_entity.verification;
          var status = verification.status;
          if (status === 'verified') {
            return;
          } else if (status === 'pending') {
            return q.reject(new Error('Identity verification pending'));
          } else if (status === 'unverified') {
            if (verification.details) {
              return q.reject(new Error(verification.details));
            } else {
              return q.reject(new Error('Identity unverified, reason unknown'));
            }
          }
        } else {
          return q.reject(new Error('Identity verification status missing from Stripe account'));
        }
      });
  };
  /**
   * Charges a stripe customer
   * @function chargeCustomer
   * @memberof BankProvider
   * @instance
   * @param {string} customer the id of the stripe customer to charge
   * @param {number} cents the numbers of cents to charge
   * @param {string} accountID (optional) the id of the stripe account to credit
   * @param {string} description intended for end users to read, such as in a bank statement
   * @returns {promise}
   */
  this.chargeCustomer = function (customer, cents, accountID, description) {
    if (!description) {
      description = 'Castle';
    }
    var promise;
    if (accountID) {
      var charge;
      promise = _api.charges.create({
        amount: cents,
        currency: 'usd',
        customer: customer,
        expand: ['transfer'],
        destination: accountID,
        description: description
      });
    } else {
      promise = _api.charges.create({
        amount: cents,
        currency: 'usd',
        customer: customer,
        description: description
      });
    }
    return promise;
  };
  /**
   * Charges a bank account
   * @function chargeBankAccount
   * @memberof BankProvider
   * @instance
   * @param {record} bankAccount the record of the bank account to be charged
   * @param {number} cents the numbers of cents to charge
   * @returns {promise} resolves with the newly created payment record
   */
  this.chargeBankAccount = function(bankAccount, cents) {
    var customer;
    var payment;
    return bankAccount.load()
      .then(function(data) {
        if (data.verifiedAt) {
          customer = data._stripe.customer_id;
          return _provider.chargeCustomer(customer, cents);
        } else {
          return q.reject(new Error('Bank accounts must be verified before they can be charged'));
        }
      })
      .then(function(charge) {
        payment = storage.createRecord('Payment');
        return payment.update({
          cents: cents,
          _stripe: {
            customer_id: customer,
            charge_id: charge.id
          }
        });
      })
      .then(function() {
        return payment;
      });
  };
  /**
   * Charges a credit card
   * @function chargeCreditCard
   * @memberof BankProvider
   * @instance
   * @param {record} card the record of the credit card to be charged
   * @param {number} cents the numbers of cents to charge
   * @returns {promise} resolves with the newly created payment record
   */
  this.chargeCreditCard = function(card, cents) {
    var customer;
    var payment;
    return card.load()
      .then(function(data) {
        customer = data._stripe.customer_id;
        return _provider.chargeCustomer(customer, cents);
      })
      .then(function(charge) {
        payment = storage.createRecord('Payment');
        return payment.update({
          cents: cents,
          _stripe: {
            customer_id: customer,
            charge_id: charge.id
          }
        });
      })
      .then(function() {
        return payment;
      });
  };
  /**
   * Transfers money from a funding source to a bank account
   * @function transfer
   * @memberof BankProvider
   * @instance
   * @param {record} source the record of the credit card or bank account to be charged
   * @param {record} destination the record of the bank account to be credited
   * @param {number} cents the number of cents to transfer
   * @param {string} description intended for end users to read, such as in a bank statement
   * @returns {promise} resolves with the newly created payment record
   */
  this.transfer = function(source, destination, cents, description) {
    var customer;
    var accountID;
    var payment;
    return source.load()
      .then(function(data) {
        customer = data._stripe.customer_id;
        return destination.load();
      })
      .then(function(data) {
        accountID = data._stripe.account_id;
        return _provider.chargeCustomer(customer, cents, accountID, description);
      })
      .then(function(charge) {
        payment = storage.createRecord('Payment');
        return payment.update({
          cents: cents,
          _stripe: {
            customer_id: customer,
            account_id: accountID,
            charge_id: charge.id
          }
        });
      })
      .then(function() {
        return payment;
      });
  };
  /**
   * Returns a bank account record, given the id
   * @function getBankAccount
   * @memberof BankProvider
   * @instance
   * @param {string} id the id of the bank account record
   * @returns {record}
   */
  this.getBankAccount = function(id) {
    return storage.createRecord('Bank Account', id);
  }
  /**
   * Returns a credit card record, given the id
   * @function getCreditCard
   * @memberof BankProvider
   * @instance
   * @param {string} id the id of the credit card record
   * @returns {record}
   */
  this.getCreditCard = function(id) {
    return storage.createRecord('Credit Card', id);
  }
};

module.exports = BankProvider;
<|endoftext|>


<commit_before>import express from 'express';
import admin from 'firebase-admin';
import transmute from 'transmutation';
import * as functions from 'firebase-functions';
import configureFacebook from './lib/facebook';

const config = functions.config();
const fb = configureFacebook(config.facebook);
admin.initializeApp(config.firebase);

const router = express();
router.post('/facebook', (req, res) => transmute({ raw: req.body })
    .extend('sender', fb.extractSender)
    .extend('action', fb.extractAction)
    .then((results) => {
        console.log(JSON.stringify(results, null, 4));
        res.sendStatus(200);
        fb.sendText(config.facebook.accesstoken, results.sender, results.action);
    })
);
router.get('/facebook', (req, res) => {
    try { fb.verifyToken(req.query); } // eslint-disable-line brace-style
    catch (err) {
        console.error(err); // eslint-disable-line no-console
        return res.sendStatus(403);
    }
    return res.status(200).send(req.query['hub.challenge']);
});

// Handle incoming messages
exports.channels = functions.https.onRequest(router);
<commit_msg>Now we can handle different messages based on action.<commit_after>import express from 'express';
import admin from 'firebase-admin';
import transmute from 'transmutation';
import * as functions from 'firebase-functions';
import configureFacebook from './lib/facebook';

const config = functions.config();
const fb = configureFacebook(config.facebook);
admin.initializeApp(config.firebase);

const responses = {
    hello: 'testing',
};
const response = ({ action }) => responses[action] || 'nothing';

const router = express();
router.post('/facebook', (req, res) => transmute({ raw: req.body })
    .extend('sender', fb.extractSender)
    .extend('action', fb.extractAction)
    .extend('message', response)
    .log()
    .then((results) => {
        res.sendStatus(200);
        fb.sendText(config.facebook.accesstoken, results.sender, results.message);
    })
);
router.get('/facebook', (req, res) => {
    try { fb.verifyToken(req.query); } // eslint-disable-line brace-style
    catch (err) {
        console.error(err); // eslint-disable-line no-console
        return res.sendStatus(403);
    }
    return res.status(200).send(req.query['hub.challenge']);
});

// Handle incoming messages
exports.channels = functions.https.onRequest(router);
<|endoftext|>


<commit_before>
/**
 * Export `seegno` shared configuration preset.
 */

module.exports = {
  env: {
    es6: true,
    mocha: true,
    node: true
  },
  extends: 'eslint:recommended',
  parser: 'babel-eslint',
  plugins: ['babel'],
  root: true,
  rules: {
    'accessor-pairs': 2,
    'array-bracket-spacing': 0,
    'arrow-parens': 0,
    'arrow-spacing': 2,
    'babel/arrow-parens': [2, 'always'],
    'babel/generator-star-spacing': [2, 'before'],
    'block-scoped-var': 2,
    'block-spacing': 0,
    'brace-style': 0,
    camelcase: 0,
    'comma-dangle': 0,
    'comma-spacing': 0,
    'comma-style': 0,
    complexity: 0,
    'computed-property-spacing': 2,
    'consistent-return': 0,
    'consistent-this': 2,
    'constructor-super': 2,
    curly: 0,
    'default-case': 2,
    'dot-location': [2, 'property'],
    'dot-notation': 0,
    'eol-last': 0,
    eqeqeq: [2, 'smart'],
    'func-names': 0,
    'func-style': [2, 'declaration', {
      allowArrowFunctions: true
    }],
    'generator-star-spacing': 0,
    'id-length': [2, {
      exceptions: ['_', 'e', 'i']
    }],
    indent: 0,
    'key-spacing': 0,
    'keyword-spacing': [0, {
      overrides: {
        case: {
          after: true
        },
        return: {
          after: true
        },
        throw: {
          after: true
        }
      }
    }],
    'linebreak-style': 0,
    'lines-around-comment': 0,
    'max-depth': 2,
    'max-nested-callbacks': 0,
    'max-params': [2, 4],
    'new-cap': 0,
    'new-parens': 2,
    'newline-after-var': 0,
    'no-alert': 2,
    'no-array-constructor': 2,
    'no-bitwise': 2,
    'no-caller': 2,
    'no-catch-shadow': 0,
    'no-class-assign': 2,
    'no-cond-assign': [2, 'always'],
    'no-confusing-arrow': 2,
    'no-const-assign': 2,
    'no-constant-condition': 2,
    'no-div-regex': 2,
    'no-dupe-class-members': 2,
    'no-else-return': 2,
    'no-empty-label': 0,
    'no-empty': 0,
    'no-eq-null': 2,
    'no-eval': 2,
    'no-extend-native': 2,
    'no-extra-bind': 2,
    'no-extra-parens': 2,
    'no-floating-decimal': 2,
    'no-implied-eval': 2,
    'no-inline-comments': 2,
    'no-iterator': 2,
    'no-label-var': 0,
    'no-labels': 0,
    'no-lone-blocks': 2,
    'no-lonely-if': 2,
    'no-loop-func': 2,
    'no-mixed-requires': 2,
    'no-mixed-spaces-and-tabs': 0,
    'no-multi-spaces': 0,
    'no-multi-str': 0,
    'no-native-reassign': 2,
    'no-nested-ternary': 2,
    'no-new-func': 2,
    'no-new-object': 2,
    'no-new-require': 2,
    'no-new-wrappers': 2,
    'no-new': 2,
    'no-octal-escape': 2,
    'no-path-concat': 2,
    'no-process-env': 2,
    'no-process-exit': 2,
    'no-proto': 2,
    'no-restricted-modules': 2,
    'no-return-assign': 2,
    'no-script-url': 2,
    'no-self-compare': 2,
    'no-sequences': 2,
    'no-shadow-restricted-names': 2,
    'no-shadow': 0,
    'no-spaced-func': 0,
    'no-sync': 2,
    'no-this-before-super': 2,
    'no-throw-literal': 2,
    'no-trailing-spaces': 2,
    'no-undef-init': 2,
    'no-undefined': 0,
    'no-underscore-dangle': 0,
    'no-unexpected-multiline': 2,
    'no-unneeded-ternary': 2,
    'no-unused-expressions': 2,
    'no-use-before-define': 2,
    'no-useless-call': 2,
    'no-useless-concat': 2,
    'no-var': 2,
    'no-void': 2,
    'no-warning-comments': 0,
    'no-with': 0,
    'object-curly-spacing': 0,
    'object-shorthand': 2,
    'one-var': 0,
    'operator-assignment': 2,
    'operator-linebreak': 0,
    'padded-blocks': 0,
    'prefer-arrow-callback': 2,
    'prefer-const': 2,
    'prefer-spread': 2,
    'prefer-template': 2,
    quotes: 0,
    'quote-props': 0,
    radix: 2,
    'require-yield': 2,
    semi: 0,
    'semi-spacing': 0,
    'sort-vars': 2,
    'space-before-blocks': 0,
    'space-before-function-paren': 0,
    'space-in-parens': 0,
    'space-infix-ops': 0,
    'space-unary-ops': 0,
    'spaced-comment': 0,
    strict: 0,
    'valid-jsdoc': 2,
    'vars-on-top': 2,
    yoda: 0,
    'wrap-iife': 0,
    'wrap-regex': 2
  }
}
<commit_msg>Require arrow parentheses as needed<commit_after>
/**
 * Export `seegno` shared configuration preset.
 */

module.exports = {
  env: {
    es6: true,
    mocha: true,
    node: true
  },
  extends: 'eslint:recommended',
  parser: 'babel-eslint',
  plugins: ['babel'],
  root: true,
  rules: {
    'accessor-pairs': 2,
    'array-bracket-spacing': 0,
    'arrow-parens': 0,
    'arrow-spacing': 2,
    'babel/arrow-parens': [2, 'as-needed'],
    'babel/generator-star-spacing': [2, 'before'],
    'block-scoped-var': 2,
    'block-spacing': 0,
    'brace-style': 0,
    camelcase: 0,
    'comma-dangle': 0,
    'comma-spacing': 0,
    'comma-style': 0,
    complexity: 0,
    'computed-property-spacing': 2,
    'consistent-return': 0,
    'consistent-this': 2,
    'constructor-super': 2,
    curly: 0,
    'default-case': 2,
    'dot-location': [2, 'property'],
    'dot-notation': 0,
    'eol-last': 0,
    eqeqeq: [2, 'smart'],
    'func-names': 0,
    'func-style': [2, 'declaration', {
      allowArrowFunctions: true
    }],
    'generator-star-spacing': 0,
    'id-length': [2, {
      exceptions: ['_', 'e', 'i']
    }],
    indent: 0,
    'key-spacing': 0,
    'keyword-spacing': [0, {
      overrides: {
        case: {
          after: true
        },
        return: {
          after: true
        },
        throw: {
          after: true
        }
      }
    }],
    'linebreak-style': 0,
    'lines-around-comment': 0,
    'max-depth': 2,
    'max-nested-callbacks': 0,
    'max-params': [2, 4],
    'new-cap': 0,
    'new-parens': 2,
    'newline-after-var': 0,
    'no-alert': 2,
    'no-array-constructor': 2,
    'no-bitwise': 2,
    'no-caller': 2,
    'no-catch-shadow': 0,
    'no-class-assign': 2,
    'no-cond-assign': [2, 'always'],
    'no-confusing-arrow': 2,
    'no-const-assign': 2,
    'no-constant-condition': 2,
    'no-div-regex': 2,
    'no-dupe-class-members': 2,
    'no-else-return': 2,
    'no-empty-label': 0,
    'no-empty': 0,
    'no-eq-null': 2,
    'no-eval': 2,
    'no-extend-native': 2,
    'no-extra-bind': 2,
    'no-extra-parens': 2,
    'no-floating-decimal': 2,
    'no-implied-eval': 2,
    'no-inline-comments': 2,
    'no-iterator': 2,
    'no-label-var': 0,
    'no-labels': 0,
    'no-lone-blocks': 2,
    'no-lonely-if': 2,
    'no-loop-func': 2,
    'no-mixed-requires': 2,
    'no-mixed-spaces-and-tabs': 0,
    'no-multi-spaces': 0,
    'no-multi-str': 0,
    'no-native-reassign': 2,
    'no-nested-ternary': 2,
    'no-new-func': 2,
    'no-new-object': 2,
    'no-new-require': 2,
    'no-new-wrappers': 2,
    'no-new': 2,
    'no-octal-escape': 2,
    'no-path-concat': 2,
    'no-process-env': 2,
    'no-process-exit': 2,
    'no-proto': 2,
    'no-restricted-modules': 2,
    'no-return-assign': 2,
    'no-script-url': 2,
    'no-self-compare': 2,
    'no-sequences': 2,
    'no-shadow-restricted-names': 2,
    'no-shadow': 0,
    'no-spaced-func': 0,
    'no-sync': 2,
    'no-this-before-super': 2,
    'no-throw-literal': 2,
    'no-trailing-spaces': 2,
    'no-undef-init': 2,
    'no-undefined': 0,
    'no-underscore-dangle': 0,
    'no-unexpected-multiline': 2,
    'no-unneeded-ternary': 2,
    'no-unused-expressions': 2,
    'no-use-before-define': 2,
    'no-useless-call': 2,
    'no-useless-concat': 2,
    'no-var': 2,
    'no-void': 2,
    'no-warning-comments': 0,
    'no-with': 0,
    'object-curly-spacing': 0,
    'object-shorthand': 2,
    'one-var': 0,
    'operator-assignment': 2,
    'operator-linebreak': 0,
    'padded-blocks': 0,
    'prefer-arrow-callback': 2,
    'prefer-const': 2,
    'prefer-spread': 2,
    'prefer-template': 2,
    quotes: 0,
    'quote-props': 0,
    radix: 2,
    'require-yield': 2,
    semi: 0,
    'semi-spacing': 0,
    'sort-vars': 2,
    'space-before-blocks': 0,
    'space-before-function-paren': 0,
    'space-in-parens': 0,
    'space-infix-ops': 0,
    'space-unary-ops': 0,
    'spaced-comment': 0,
    strict: 0,
    'valid-jsdoc': 2,
    'vars-on-top': 2,
    yoda: 0,
    'wrap-iife': 0,
    'wrap-regex': 2
  }
}
<|endoftext|>


<commit_before>'use strict';

import { EOL } from 'os';
import {compose, serialize} from 'yaml-js';
import {load, dump} from 'js-yaml';
import {
  isArray,
  isString,
  isObject,
  isUndefined,
  isNull,
  isNumber,
  isEqual,
  repeat,
  each,
  contains,
  last
} from 'lodash';

import YAWNError from './error.js';

const NULL_TAG = 'tag:yaml.org,2002:null';
const STR_TAG = 'tag:yaml.org,2002:str';
const INT_TAG = 'tag:yaml.org,2002:int';
const FLOAT_TAG = 'tag:yaml.org,2002:float';
const MAP_TAG = 'tag:yaml.org,2002:map';
const SEQ_TAG = 'tag:yaml.org,2002:seq';

const LINE_SEPARATOR = EOL;
const SPACE = ' ';
const DASH = '-';

// export default class YAWN {
export default class YAWN {

  constructor(str) {
    if (!isString(str)) {
      throw new TypeError('str should be a string');
    }

    this.yaml = str;
  }

  get json() {
    return load(this.yaml);
  }

  set json(newJson) {

    // if json is not changed do nothing
    if (isEqual(this.json, newJson)) {
      return;
    }

    const ast = compose(this.yaml);

    if (isUndefined(newJson)) {
      this.yaml = '';
      return;
    }

    // -------------------------------------------------------------------------
    // check if entire json is changed
    // -------------------------------------------------------------------------
    let newTag = getTag(newJson);

    if (ast.tag !== newTag) {
      let newYaml = cleanDump(newJson);

      // replace this.yaml value from start to end mark with newYaml if node is
      // primitive
      if (!isObject(newJson)) {
        this.yaml = replacePrimitive(ast, newYaml, this.yaml);

      // if node is not primitive
      } else {
        this.yaml = replaceNode(ast, newYaml, this.yaml);
      }

      return;
    }

    // -------------------------------------------------------------------------
    // NULL_TAG, STR_TAG, INT_TAG, FLOAT_TAG
    // -------------------------------------------------------------------------
    if (contains([NULL_TAG, STR_TAG, INT_TAG, FLOAT_TAG], ast.tag)) {
      this.yaml = replacePrimitive(ast, newJson, this.yaml);

      return;
    }

    // -------------------------------------------------------------------------
    // MAP_TAG
    // -------------------------------------------------------------------------
    if (ast.tag === MAP_TAG) {
      let json = this.json;

      this.yaml = updateMap(ast, newJson, json, this.yaml);
    }

    // -------------------------------------------------------------------------
    // SEQ_TAG
    // -------------------------------------------------------------------------
    if (ast.tag === SEQ_TAG) {
      this.yaml = updateSeq(ast, newJson, this.yaml);
    }

    // Trim trailing whitespaces
    this.yaml = this.yaml
      .split(LINE_SEPARATOR)
      .map(line=> line.replace(/[ \t]+$/, ''))
      .join(LINE_SEPARATOR);
  }

  toString() {
    return this.yaml;
  }

  toJSON() {
    return this.json;
  }
}

/*
 * Determines the AST tag of a JSON object
 *
 * @param {any} - json
 * @returns {boolean}
 * @throws {YAWNError} - if json has weird type
*/
function getTag(json) {
  let tag = null;

  if (isArray(json)) {
    tag = SEQ_TAG;
  } else if (isObject(json)) {
    tag = MAP_TAG;
  } else if (isNull(json)) {
    tag = NULL_TAG;
  } else if (isNumber(json)) {
    if (json % 10 === 0) {
      tag = INT_TAG;
    } else {
      tag = FLOAT_TAG;
    }
  } else if (isString(json)) {
    tag = STR_TAG;
  } else {
    throw new YAWNError('Unknown type');
  }
  return tag;
}

/*
 * Update a sequence with new JSON
 *
 * @param {Node} ast
 * @param {object} newJson
 * @param {string} yaml
 *
 * @returns {string}
 *
*/
function updateSeq(ast, newJson, yaml) {
  let values = load(serialize(ast));

  // new items in newJson
  let newItems = differenceWith(newJson, values, isEqual).reverse();

  if (newItems.length) {
    yaml = insertAfterNode(ast, cleanDump(newItems), yaml);
  }

  // deleted items in newJson
  let deletedItems = differenceWith(values, newJson, isEqual);

  deletedItems.forEach(deletedItem=> {

    // find the node for this item
    each(ast.value, node => {
      if (isEqual(load(serialize(node)), deletedItem)) {

        // remove it from yaml
        yaml = removeArrayElement(node, yaml);

        // re-compose the AST for accurate removals after
        ast = compose(yaml);
      }
    });
  });

  return yaml;
}

/*
 * update a map structure with new values
 *
 * @param {AST} ast - a map AST
 * @param {any} newJson
 * @param {any} - json
 * @param {string} yaml
 * @returns {boolean}
 * @throws {YAWNError} - if json has weird type
*/
function updateMap(ast, newJson, json, yaml) {

  // look for changes
  each(ast.value, pair => {
    let [keyNode, valNode] = pair;

    // node is deleted
    if (isUndefined(newJson[keyNode.value])) {

      // TODO: can we use of the methods below?
      yaml = yaml.substr(0, keyNode.start_mark.pointer) +
        yaml.substring(getNodeEndMark(valNode).pointer);
      return;
    }

    let value = json[keyNode.value];
    let newValue = newJson[keyNode.value];

    // primitive value has changed
    if (newValue !== value && !isArray(valNode.value)) {

      // replace the value node
      yaml = replacePrimitive(valNode, newValue, yaml);

      // remove the key/value from newJson so it's not detected as new pair in
      // later code
      delete newJson[keyNode.value];
    }

    // non primitive value has changed
    if (!isEqual(newValue, value) && isArray(valNode.value)) {

      // array value has changed
      if (isArray(newValue)) {

        // recurse
        yaml = updateSeq(valNode, newValue, yaml);

      // map value has changed
      } else {

        // recurse
        yaml = updateMap(valNode, newValue, value, yaml);

        ast = compose(yaml);

        // remove the key/value from newJson so it's not detected as new pair in
        // later code
        delete newJson[keyNode.value];
      }
    }
  });

  // look for new items to add
  each(newJson, (value, key)=> {

    // item is new
    if (isUndefined(json[key])) {
      let newValue = cleanDump({[key]: value});

      yaml = insertAfterNode(ast, newValue, yaml);
    }
  });

  return yaml;
}

/*
 * Place value in node range in yaml string
 *
 * @param node {Node}
 * @param value {any}
 * @param yaml {string}
 *
 * @returns {string}
*/
function replacePrimitive(node, value, yaml) {
  return yaml.substr(0, node.start_mark.pointer) +
    String(value) +
    yaml.substring(node.end_mark.pointer);
}

/*
 * Place value in node range in yaml string
 *
 * @param node {Node}
 * @param value {any}
 * @param yaml {string}
 *
 * @returns {string}
*/
function replaceNode(node, value, yaml) {
  let indentedValue = indent(value, node.start_mark.column);
  let lineStart = node.start_mark.pointer - node.start_mark.column;

  return yaml.substr(0, lineStart) +
    indentedValue +
    yaml.substring(getNodeEndMark(node).pointer);
}

/*
 * Place value after node range in yaml string
 *
 * @param node {Node}
 * @param value {any}
 * @param yaml {string}
 *
 * @returns {string}
*/
function insertAfterNode(node, value, yaml) {
  let indentedValue = indent(value, node.start_mark.column);

  return yaml.substr(0, getNodeEndMark(node).pointer) +
    LINE_SEPARATOR +
    indentedValue +
    yaml.substring(getNodeEndMark(node).pointer);
}

/*
 * Removes a node from array
 *
 * @param {Node} node
 * @param {string} yaml
 *
 * @returns {string}
*/
function removeArrayElement(node, yaml) {

  // FIXME: Removing element from a YAML like `[a,b]` won't work with this.

  // find index of DASH(`-`) character for this array
  let index = node.start_mark.pointer;
  while (index > 0 && yaml[index] !== DASH) {
    index--;
  }

  return yaml.substr(0, index) +
      yaml.substring(getNodeEndMark(node).pointer);
}

/*
 * Gets end mark of an AST
 *
 * @param {Node} ast
 *
 * @retusns {Mark}
*/
function getNodeEndMark(ast) {
  if (isArray(ast.value) && ast.value.length) {
    let lastItem = last(ast.value);

    if (isArray(lastItem) && lastItem.length) {
      return getNodeEndMark(last(lastItem));
    }

    return getNodeEndMark(lastItem);
  }

  return ast.end_mark;
}

/*
 * Indents a string with number of characters
 *
 * @param {string} str
 * @param {integer} depth - can be negative also
 *
 * @returns {string}
*/
function indent(str, depth) {
  return str
    .split(LINE_SEPARATOR)
    .filter(line => line)
    .map(line => repeat(SPACE, depth) + line)
    .join(LINE_SEPARATOR);
}

/*
 * Dump a value to YAML sting without the trailing new line
 *
 * @param {any} value
 *
 * @returns {string}
 *
*/
function cleanDump(value) {
  return dump(value).replace(/\n$/, '');
}

/*
 * find difference between two arrays by using a comparison function
 *
 * @param {array<any>} src
 * @param {array<any>} dest
 * @param {function} compFn
 *
 * @returns {array}
*/
function differenceWith(src, dest, compFn) {
  return src.filter(srcItem=> {
    return dest.every(destItem=> {
      return !compFn(srcItem, destItem);
    });
  });
}
<commit_msg>erge branch 'bugfix/cross-platform-line-endings' of https://github.com/kevinswiber/yawn-yaml into kevinswiber-bugfix/cross-platform-line-endings<commit_after>'use strict';

import { EOL } from 'os';
import {compose, serialize} from 'yaml-js';
import {load, dump} from 'js-yaml';
import {
  isArray,
  isString,
  isObject,
  isUndefined,
  isNull,
  isNumber,
  isEqual,
  repeat,
  each,
  contains,
  last
} from 'lodash';

import YAWNError from './error.js';

const NULL_TAG = 'tag:yaml.org,2002:null';
const STR_TAG = 'tag:yaml.org,2002:str';
const INT_TAG = 'tag:yaml.org,2002:int';
const FLOAT_TAG = 'tag:yaml.org,2002:float';
const MAP_TAG = 'tag:yaml.org,2002:map';
const SEQ_TAG = 'tag:yaml.org,2002:seq';

const SPACE = ' ';
const DASH = '-';

// export default class YAWN {
export default class YAWN {

  constructor(str) {
    if (!isString(str)) {
      throw new TypeError('str should be a string');
    }

    this.yaml = str;
  }

  get json() {
    return load(this.yaml);
  }

  set json(newJson) {

    // if json is not changed do nothing
    if (isEqual(this.json, newJson)) {
      return;
    }

    const ast = compose(this.yaml);

    if (isUndefined(newJson)) {
      this.yaml = '';
      return;
    }

    // -------------------------------------------------------------------------
    // check if entire json is changed
    // -------------------------------------------------------------------------
    let newTag = getTag(newJson);

    if (ast.tag !== newTag) {
      let newYaml = cleanDump(newJson);

      // replace this.yaml value from start to end mark with newYaml if node is
      // primitive
      if (!isObject(newJson)) {
        this.yaml = replacePrimitive(ast, newYaml, this.yaml);

      // if node is not primitive
      } else {
        this.yaml = replaceNode(ast, newYaml, this.yaml);
      }

      return;
    }

    // -------------------------------------------------------------------------
    // NULL_TAG, STR_TAG, INT_TAG, FLOAT_TAG
    // -------------------------------------------------------------------------
    if (contains([NULL_TAG, STR_TAG, INT_TAG, FLOAT_TAG], ast.tag)) {
      this.yaml = replacePrimitive(ast, newJson, this.yaml);

      return;
    }

    // -------------------------------------------------------------------------
    // MAP_TAG
    // -------------------------------------------------------------------------
    if (ast.tag === MAP_TAG) {
      let json = this.json;

      this.yaml = updateMap(ast, newJson, json, this.yaml);
    }

    // -------------------------------------------------------------------------
    // SEQ_TAG
    // -------------------------------------------------------------------------
    if (ast.tag === SEQ_TAG) {
      this.yaml = updateSeq(ast, newJson, this.yaml);
    }

    // Trim trailing whitespaces
    this.yaml = this.yaml
      .split(EOL)
      .map(line=> line.replace(/[ \t]+$/, ''))
      .join(EOL);
  }

  toString() {
    return this.yaml;
  }

  toJSON() {
    return this.json;
  }
}

/*
 * Determines the AST tag of a JSON object
 *
 * @param {any} - json
 * @returns {boolean}
 * @throws {YAWNError} - if json has weird type
*/
function getTag(json) {
  let tag = null;

  if (isArray(json)) {
    tag = SEQ_TAG;
  } else if (isObject(json)) {
    tag = MAP_TAG;
  } else if (isNull(json)) {
    tag = NULL_TAG;
  } else if (isNumber(json)) {
    if (json % 10 === 0) {
      tag = INT_TAG;
    } else {
      tag = FLOAT_TAG;
    }
  } else if (isString(json)) {
    tag = STR_TAG;
  } else {
    throw new YAWNError('Unknown type');
  }
  return tag;
}

/*
 * Update a sequence with new JSON
 *
 * @param {Node} ast
 * @param {object} newJson
 * @param {string} yaml
 *
 * @returns {string}
 *
*/
function updateSeq(ast, newJson, yaml) {
  let values = load(serialize(ast));

  // new items in newJson
  let newItems = differenceWith(newJson, values, isEqual).reverse();

  if (newItems.length) {
    yaml = insertAfterNode(ast, cleanDump(newItems), yaml);
  }

  // deleted items in newJson
  let deletedItems = differenceWith(values, newJson, isEqual);

  deletedItems.forEach(deletedItem=> {

    // find the node for this item
    each(ast.value, node => {
      if (isEqual(load(serialize(node)), deletedItem)) {

        // remove it from yaml
        yaml = removeArrayElement(node, yaml);

        // re-compose the AST for accurate removals after
        ast = compose(yaml);
      }
    });
  });

  return yaml;
}

/*
 * update a map structure with new values
 *
 * @param {AST} ast - a map AST
 * @param {any} newJson
 * @param {any} - json
 * @param {string} yaml
 * @returns {boolean}
 * @throws {YAWNError} - if json has weird type
*/
function updateMap(ast, newJson, json, yaml) {

  // look for changes
  each(ast.value, pair => {
    let [keyNode, valNode] = pair;

    // node is deleted
    if (isUndefined(newJson[keyNode.value])) {

      // TODO: can we use of the methods below?
      yaml = yaml.substr(0, keyNode.start_mark.pointer) +
        yaml.substring(getNodeEndMark(valNode).pointer);
      return;
    }

    let value = json[keyNode.value];
    let newValue = newJson[keyNode.value];

    // primitive value has changed
    if (newValue !== value && !isArray(valNode.value)) {

      // replace the value node
      yaml = replacePrimitive(valNode, newValue, yaml);

      // remove the key/value from newJson so it's not detected as new pair in
      // later code
      delete newJson[keyNode.value];
    }

    // non primitive value has changed
    if (!isEqual(newValue, value) && isArray(valNode.value)) {

      // array value has changed
      if (isArray(newValue)) {

        // recurse
        yaml = updateSeq(valNode, newValue, yaml);

      // map value has changed
      } else {

        // recurse
        yaml = updateMap(valNode, newValue, value, yaml);

        ast = compose(yaml);

        // remove the key/value from newJson so it's not detected as new pair in
        // later code
        delete newJson[keyNode.value];
      }
    }
  });

  // look for new items to add
  each(newJson, (value, key)=> {

    // item is new
    if (isUndefined(json[key])) {
      let newValue = cleanDump({[key]: value});

      yaml = insertAfterNode(ast, newValue, yaml);
    }
  });

  return yaml;
}

/*
 * Place value in node range in yaml string
 *
 * @param node {Node}
 * @param value {any}
 * @param yaml {string}
 *
 * @returns {string}
*/
function replacePrimitive(node, value, yaml) {
  return yaml.substr(0, node.start_mark.pointer) +
    String(value) +
    yaml.substring(node.end_mark.pointer);
}

/*
 * Place value in node range in yaml string
 *
 * @param node {Node}
 * @param value {any}
 * @param yaml {string}
 *
 * @returns {string}
*/
function replaceNode(node, value, yaml) {
  let indentedValue = indent(value, node.start_mark.column);
  let lineStart = node.start_mark.pointer - node.start_mark.column;

  return yaml.substr(0, lineStart) +
    indentedValue +
    yaml.substring(getNodeEndMark(node).pointer);
}

/*
 * Place value after node range in yaml string
 *
 * @param node {Node}
 * @param value {any}
 * @param yaml {string}
 *
 * @returns {string}
*/
function insertAfterNode(node, value, yaml) {
  let indentedValue = indent(value, node.start_mark.column);

  return yaml.substr(0, getNodeEndMark(node).pointer) +
    EOL +
    indentedValue +
    yaml.substring(getNodeEndMark(node).pointer);
}

/*
 * Removes a node from array
 *
 * @param {Node} node
 * @param {string} yaml
 *
 * @returns {string}
*/
function removeArrayElement(node, yaml) {

  // FIXME: Removing element from a YAML like `[a,b]` won't work with this.

  // find index of DASH(`-`) character for this array
  let index = node.start_mark.pointer;
  while (index > 0 && yaml[index] !== DASH) {
    index--;
  }

  return yaml.substr(0, index) +
      yaml.substring(getNodeEndMark(node).pointer);
}

/*
 * Gets end mark of an AST
 *
 * @param {Node} ast
 *
 * @retusns {Mark}
*/
function getNodeEndMark(ast) {
  if (isArray(ast.value) && ast.value.length) {
    let lastItem = last(ast.value);

    if (isArray(lastItem) && lastItem.length) {
      return getNodeEndMark(last(lastItem));
    }

    return getNodeEndMark(lastItem);
  }

  return ast.end_mark;
}

/*
 * Indents a string with number of characters
 *
 * @param {string} str
 * @param {integer} depth - can be negative also
 *
 * @returns {string}
*/
function indent(str, depth) {
  return str
    .split(EOL)
    .filter(line => line)
    .map(line => repeat(SPACE, depth) + line)
    .join(EOL);
}

/*
 * Dump a value to YAML sting without the trailing new line
 *
 * @param {any} value
 *
 * @returns {string}
 *
*/
function cleanDump(value) {
  let yaml = dump(value).replace(/\n$/, '');

  if (EOL !== '\n') {
    yaml = yaml.replace(/\n/g, EOL);
  }

  return yaml;
}

/*
 * find difference between two arrays by using a comparison function
 *
 * @param {array<any>} src
 * @param {array<any>} dest
 * @param {function} compFn
 *
 * @returns {array}
*/
function differenceWith(src, dest, compFn) {
  return src.filter(srcItem=> {
    return dest.every(destItem=> {
      return !compFn(srcItem, destItem);
    });
  });
}
<|endoftext|>


<commit_before>import stylelint from 'stylelint'

const ruleName = 'scale-unlimited/declaration-strict-value'
const utils = stylelint.utils
const messages = utils.ruleMessages(ruleName, {
  expected: (type, value, property) => {
    if (Array.isArray(type)) {
      const typeLast = type.pop()

      type = type.length ? `${type.join(', ')} or ${typeLast}` : typeLast
    }

    return `Expected ${type} for ${value} of ${property}`
  },
})
const reVar = /^(?:@.+|\$.+|var\(--.+\))$/
const reFunc = /^.+\(.+\)$/
const defaults = {
  ignoreFunctions: true,
  ignoreKeywords: null,
}

const getIgnoredKeywords = (ignoreKeywords, property) => {
  if (!ignoreKeywords) return null

  const keywords = ignoreKeywords[property] || ignoreKeywords[''] || ignoreKeywords

  return Array.isArray(keywords) ? keywords : [keywords]
}

const rule = (properties, options) =>
  (root, result) => {
    const hasValidOptions = utils.validateOptions(
      result,
      ruleName,
      {
        actual: properties,
        possible: validProperties,
      },
      {
        actual: options,
        possible: validOptions,
        optional: true,
      }
    )

    if (!hasValidOptions) return

    if (!Array.isArray(properties)) {
      properties = [properties]
    }

    const { ignoreKeywords, ignoreFunctions } = {
      ...defaults,
      ...options,
    }
    const reKeywords = ignoreKeywords ? {} : null

    properties.forEach((property) => {
      let propFilter = property

      if (propFilter.charAt(0) === '/' && propFilter.slice(-1) === '/') {
        propFilter = new RegExp(propFilter.slice(1, -1))
      }

      root.walkDecls(propFilter, declsWalker)

      function declsWalker(node) {
        const { value, prop } = node
        const validVar = reVar.test(value)
        let validFunc = false
        let validKeyword = false

        if (ignoreFunctions && !validVar) {
          validFunc = reFunc.test(value)
        }

        if (ignoreKeywords && (!validVar || !validFunc)) {
          let reKeyword = reKeywords[property]

          if (!reKeyword) {
            const ignoreKeyword = getIgnoredKeywords(ignoreKeywords, property)

            if (ignoreKeyword) {
              reKeyword = new RegExp(`^${ignoreKeyword.join('|')}$`)
              reKeywords[property] = reKeyword
            }
          }

          if (reKeyword) {
            validKeyword = reKeyword.test(value)
          }
        }

        if (!validVar && !validFunc && !validKeyword) {
          const type = ['variable']

          if (ignoreFunctions) {
            type.push('function')
          }

          if (ignoreKeywords && getIgnoredKeywords(ignoreKeywords, property)) {
            type.push('keyword')
          }

          const { raws } = node
          const { start } = node.source

          utils.report({
            ruleName,
            result,
            node,
            line: start.line,
            column: start.column + prop.length + raws.between.length,
            message: messages.expected(type, value, prop),
          })
        }
      }
    })
  }

rule.primaryOptionArray = true

const declarationStrictValuePlugin = stylelint.createPlugin(ruleName, rule)

export default declarationStrictValuePlugin
export { ruleName, messages }

function validProperties(actual) {
  return typeof actual === 'string' ||
    (Array.isArray(actual) && actual.every(item => typeof item === 'string'))
}

function validOptions(actual) {
  if (typeof actual !== 'object') return false

  const allowedKeys = Object.keys(defaults)
  if (!Object.keys(actual).every(key => allowedKeys.indexOf(key) > -1)) return false

  if ('ignoreFunctions' in actual &&
    typeof actual.ignoreFunctions !== 'boolean' &&
    actual.ignoreFunctions !== null) return false

  if ('ignoreKeywords' in actual &&
    !validProperties(actual.ignoreKeywords) &&
    !validHash(actual.ignoreKeywords)) return false

  return true
}

function validHash(actual) {
  if (typeof actual !== 'object') return false

  return Object.keys(actual).every(key => validProperties(actual[key]))
}
<commit_msg>improved expected message<commit_after>import stylelint from 'stylelint'

const ruleName = 'scale-unlimited/declaration-strict-value'
const utils = stylelint.utils
const messages = utils.ruleMessages(ruleName, {
  expected: (type, value, property) => {
    if (Array.isArray(type)) {
      const typeLast = type.pop()

      type = type.length ? `${type.join(', ')} or ${typeLast}` : typeLast
    }

    return `Expected ${type} for "${value}" of "${property}"`
  },
})
const reVar = /^(?:@.+|\$.+|var\(--.+\))$/
const reFunc = /^.+\(.+\)$/
const defaults = {
  ignoreFunctions: true,
  ignoreKeywords: null,
}

const getIgnoredKeywords = (ignoreKeywords, property) => {
  if (!ignoreKeywords) return null

  const keywords = ignoreKeywords[property] || ignoreKeywords[''] || ignoreKeywords

  return Array.isArray(keywords) ? keywords : [keywords]
}

const rule = (properties, options) =>
  (root, result) => {
    const hasValidOptions = utils.validateOptions(
      result,
      ruleName,
      {
        actual: properties,
        possible: validProperties,
      },
      {
        actual: options,
        possible: validOptions,
        optional: true,
      }
    )

    if (!hasValidOptions) return

    if (!Array.isArray(properties)) {
      properties = [properties]
    }

    const { ignoreKeywords, ignoreFunctions } = {
      ...defaults,
      ...options,
    }
    const reKeywords = ignoreKeywords ? {} : null

    properties.forEach((property) => {
      let propFilter = property

      if (propFilter.charAt(0) === '/' && propFilter.slice(-1) === '/') {
        propFilter = new RegExp(propFilter.slice(1, -1))
      }

      root.walkDecls(propFilter, declsWalker)

      function declsWalker(node) {
        const { value, prop } = node
        const validVar = reVar.test(value)
        let validFunc = false
        let validKeyword = false

        if (ignoreFunctions && !validVar) {
          validFunc = reFunc.test(value)
        }

        if (ignoreKeywords && (!validVar || !validFunc)) {
          let reKeyword = reKeywords[property]

          if (!reKeyword) {
            const ignoreKeyword = getIgnoredKeywords(ignoreKeywords, property)

            if (ignoreKeyword) {
              reKeyword = new RegExp(`^${ignoreKeyword.join('|')}$`)
              reKeywords[property] = reKeyword
            }
          }

          if (reKeyword) {
            validKeyword = reKeyword.test(value)
          }
        }

        if (!validVar && !validFunc && !validKeyword) {
          const type = ['variable']

          if (ignoreFunctions) {
            type.push('function')
          }

          if (ignoreKeywords && getIgnoredKeywords(ignoreKeywords, property)) {
            type.push('keyword')
          }

          const { raws } = node
          const { start } = node.source

          utils.report({
            ruleName,
            result,
            node,
            line: start.line,
            column: start.column + prop.length + raws.between.length,
            message: messages.expected(type, value, prop),
          })
        }
      }
    })
  }

rule.primaryOptionArray = true

const declarationStrictValuePlugin = stylelint.createPlugin(ruleName, rule)

export default declarationStrictValuePlugin
export { ruleName, messages }

function validProperties(actual) {
  return typeof actual === 'string' ||
    (Array.isArray(actual) && actual.every(item => typeof item === 'string'))
}

function validOptions(actual) {
  if (typeof actual !== 'object') return false

  const allowedKeys = Object.keys(defaults)
  if (!Object.keys(actual).every(key => allowedKeys.indexOf(key) > -1)) return false

  if ('ignoreFunctions' in actual &&
    typeof actual.ignoreFunctions !== 'boolean' &&
    actual.ignoreFunctions !== null) return false

  if ('ignoreKeywords' in actual &&
    !validProperties(actual.ignoreKeywords) &&
    !validHash(actual.ignoreKeywords)) return false

  return true
}

function validHash(actual) {
  if (typeof actual !== 'object') return false

  return Object.keys(actual).every(key => validProperties(actual[key]))
}
<|endoftext|>


<commit_before>var url = require("url");
var clone = require("clone");
var extend = require("extend");
var sortedObject = require("sorted-object");
var baseUrl = url.parse("https://www.streamtext.net/player", true);

// For a list of available query parameters, see
//   https://streamtext.zendesk.com/entries/21721966-controlling-the-streaming-text-page-display
var queryParams = {
    chat: "off",
    header: "off",
    title: "off",
    footer: "off",
    scroll: "off",
    fgc: "fff",
    bgc: "transparent",
    ff: "helvetica,arial,sans-serif",
    fs: "42",
    spacing: "1.8",
    "content-style": "overflow:hidden; text-transform: uppercase"
};

function createStreamTextUrl(eventId) {
    var urlObj = clone(baseUrl);
    urlObj.query = sortedObject(extend(clone(queryParams), { event: eventId }));
    return url.format(urlObj);
}

function extractEventId(streamTextUrl) {
    var urlObj = url.parse(streamTextUrl, true);
    return urlObj.query.event;
}

function createIframe(streamTextUrl) {
    var iframe = document.createElement("iframe");
    iframe.src = streamTextUrl;
    iframe.className = "content";
    iframe.id = "stream-text-embed";
    return iframe;
}

function getStreamTextUrl() {
    var inputUrl = window.prompt("Enter URL");  // eslint-disable-line no-alert
    return createStreamTextUrl(extractEventId(inputUrl, true));
}

document.getElementById("stream-text-container").appendChild(createIframe(getStreamTextUrl()));
<commit_msg>Wrap JS with an IIFE and "use strict"<commit_after>(function() {  // IIFE

"use strict";
var url = require("url");
var clone = require("clone");
var extend = require("extend");
var sortedObject = require("sorted-object");
var baseUrl = url.parse("https://www.streamtext.net/player", true);

// For a list of available query parameters, see
//   https://streamtext.zendesk.com/entries/21721966-controlling-the-streaming-text-page-display
var queryParams = {
    chat: "off",
    header: "off",
    title: "off",
    footer: "off",
    scroll: "off",
    fgc: "fff",
    bgc: "transparent",
    ff: "helvetica,arial,sans-serif",
    fs: "42",
    spacing: "1.8",
    "content-style": "overflow:hidden; text-transform: uppercase"
};

function createStreamTextUrl(eventId) {
    var urlObj = clone(baseUrl);
    urlObj.query = sortedObject(extend(clone(queryParams), { event: eventId }));
    return url.format(urlObj);
}

function extractEventId(streamTextUrl) {
    var urlObj = url.parse(streamTextUrl, true);
    return urlObj.query.event;
}

function createIframe(streamTextUrl) {
    var iframe = document.createElement("iframe");
    iframe.src = streamTextUrl;
    iframe.className = "content";
    iframe.id = "stream-text-embed";
    return iframe;
}

function getStreamTextUrl() {
    var inputUrl = window.prompt("Enter URL");  // eslint-disable-line no-alert
    return createStreamTextUrl(extractEventId(inputUrl, true));
}

document.getElementById("stream-text-container").appendChild(createIframe(getStreamTextUrl()));

})();  // IIFE
<|endoftext|>


<commit_before>var fs = require('fs');
var request = require("request");
var cheerio = require("cheerio");
var AlexaSkill = require('./AlexaSkill');

var APP_ID = 'amzn1.ask.skill.0fdc78be-c6a1-462b-9cfc-c8c1e01aeb11';

var intentHandlersUS = {
    "GetNewAlbumRatingIntent": function (intent, session, response) {
        handleAlbumRatingRequest(intent, response, 'US');
    },

    "AMAZON.HelpIntent": function (intent, session, response) {
        response.ask("You can make a request like 'Tell me the album rating of Taylor Swift's 1989!', or, you can say 'Exit!'... What can I help you with?", "What can I help you with?");
    },

    "AMAZON.StopIntent": function (intent, session, response) {
        var speechOutput = "Goodbye";
        this.emit(':tell', speechOutput);
    },

    "AMAZON.CancelIntent": function (intent, session, response) {
        var speechOutput = "Goodbye";
        this.emit(':tell', speechOutput);
    }
};

var intentHandlersUK = {
    "GetNewAlbumRatingIntent": function (intent, session, response) {
        handleAlbumRatingRequest(intent, response, 'UK');
    },

    "AMAZON.HelpIntent": function (intent, session, response) {
        response.ask("You can make a request like 'Tell me the album rating of Taylor Swift's 1989!', or, you can say 'Exit!'... What can I help you with?", "What can I help you with?");
    },

    "AMAZON.StopIntent": function (intent, session, response) {
        var speechOutput = "Goodbye";
        response.tell(speechOutput);
    },

    "AMAZON.CancelIntent": function (intent, session, response) {
        var speechOutput = "Goodbye";
        response.tell(speechOutput);
    }
};

var intentHandlersDE = {
    "GetNewAlbumRatingIntent": function (intent, session, response) {
        handleAlbumRatingRequest(intent,response, 'DE');
    },

    "AMAZON.HelpIntent": function (intent, session, response) {
        response.ask("You can make a request like 'Tell me the album rating of Taylor Swift's 1989!', or, you can say 'Exit!'... What can I help you with?", "What can I help you with?");
    },

    "AMAZON.StopIntent": function (intent, session, response) {
        var speechOutput = "Auf Wiedersehen!";
        response.tell(speechOutput);
    },

    "AMAZON.CancelIntent": function (intent, session, response) {
        var speechOutput = "Auf Wiedersehen!";
        response.tell(speechOutput);
    }
};

function handleAlbumRatingRequest(intent, response, language) {
    var album = intent.slots.Album.value? intent.slots.Album.value : '';
    var artist = intent.slots.Artist.value? intent.slots.Artist.value : '';
    var albumAndArtist = album + ' ' + artist;

    helper.getAlbumRating(albumAndArtist, function(albumReview) {
        var speechOutput = '';
        if(language !== 'DE'){
            speechOutput = "Pitchfork rated " + album + " by " + artist + " at " + albumReview.rating + ". They wrote: " + albumReview.abstract;
        } else {
            speechOutput = 'Pitchfork hat ' + album + " von " + artist + " mit " + albumReview.rating + "bewertet. In der Zusammenfassung heißt es: " + albumReview.abstract;
        }
        response.tell(speechOutput);
    }, function(errorFeedback){
        if(language !== 'DE'){
            response.tell('I couldn\'t find a result for ' + album + ' by ' + artist + '.');
        } else {
            response.tell('Ich konnte keine Ergebnisse zu ' + album + ' von ' + artist + ' finden.')
        }
    });
}

// Create the handler that responds to the Alexa Request.
exports.handler = function (event, context) {
    // Create an instance of the SpaceGeek skill.
    var skill = new AlexaSkill(APP_ID);

    var locale = event.request.locale;

    if (locale == 'en-GB'){
        skill.intentHandlers = intentHandlersUK;
    } else if (locale == 'de-DE') {
        skill.intentHandlers = intentHandlersDE;
    } else {
        skill.intentHandlers = intentHandlersUS;
    }

    skill.execute(event, context);
};

// --------------- Helper Functions  -----------------------

var helper = {

    // gives the user more information on their final choice
    giveDescription: function (context) {

        // get the speech for the child node
        var description = helper.getDescriptionForNode(context.attributes.currentNode);
        var message = description + ', ' + repeatWelcomeMessage;

        context.emit(':ask', message, message);
    },

    // logic to provide the responses to the yes or no responses to the main questions
    yesOrNo: function (context, reply) {

        // this is a question node so we need to see if the user picked yes or no
        var nextNodeId = helper.getNextNode(context.attributes.currentNode, reply);

        // error in node data
        if (nextNodeId == -1)
        {
            context.handler.state = states.STARTMODE;

            // the current node was not found in the nodes array
            // this is due to the current node in the nodes array having a yes / no node id for a node that does not exist
            context.emit(':tell', nodeNotFoundMessage, nodeNotFoundMessage);
        }

        // get the speech for the child node
        var message = helper.getSpeechForNode(nextNodeId);

        // have we made a decision
        if (helper.isAnswerNode(nextNodeId) === true) {

            // set the game state to description mode
            context.handler.state = states.DESCRIPTIONMODE;

            // append the play again prompt to the decision and speak it
            message = decisionMessage + ' ' + message + ' ,' + playAgainMessage;
        }

        // set the current node to next node we want to go to
        context.attributes.currentNode = nextNodeId;

        context.emit(':ask', message, message);
    },

    // gets the description for the given node id
    getDescriptionForNode: function (nodeId) {

        for (var i = 0; i < nodes.length; i++) {
            if (nodes[i].node == nodeId) {
                return nodes[i].description;
            }
        }
        return descriptionNotFoundMessage + nodeId;
    },

    // returns the speech for the provided node id
    getSpeechForNode: function (nodeId) {

        for (var i = 0; i < nodes.length; i++) {
            if (nodes[i].node == nodeId) {
                return nodes[i].message;
            }
        }
        return speechNotFoundMessage + nodeId;
    },

    // checks to see if this node is an choice node or a decision node
    isAnswerNode: function (nodeId) {

        for (var i = 0; i < nodes.length; i++) {
            if (nodes[i].node == nodeId) {
                if (nodes[i].yes === 0 && nodes[i].no === 0) {
                    return true;
                }
            }
        }
        return false;
    },

    // gets the next node to traverse to based on the yes no response
    getNextNode: function (nodeId, yesNo) {
        for (var i = 0; i < nodes.length; i++) {
            if (nodes[i].node == nodeId) {
                if (yesNo == "yes") {
                    return nodes[i].yes;
                }
                return nodes[i].no;
            }
        }
        // error condition, didnt find a matching node id. Cause will be a yes / no entry in the array but with no corrosponding array entry
        return -1;
    },

    // Recursively walks the node tree looking for nodes already visited
    // This method could be changed if you want to implement another type of checking mechanism
    // This should be run on debug builds only not production
    // returns false if node tree path does not contain any previously visited nodes, true if it finds one
    debugFunction_walkNode: function (nodeId) {

        // console.log("Walking node: " + nodeId);

        if( helper.isAnswerNode(nodeId) === true) {
            // found an answer node - this path to this node does not contain a previously visted node
            // so we will return without recursing further

            // console.log("Answer node found");
             return false;
        }

        // mark this question node as visited
        if( helper.debugFunction_AddToVisited(nodeId) === false)
        {
            // node was not added to the visited list as it already exists, this indicates a duplicate path in the tree
            return true;
        }

        // console.log("Recursing yes path");
        var yesNode = helper.getNextNode(nodeId, "yes");
        var duplicatePathHit = helper.debugFunction_walkNode(yesNode);

        if( duplicatePathHit === true){
            return true;
        }

        // console.log("Recursing no");
        var noNode = helper.getNextNode(nodeId, "no");
        duplicatePathHit = helper.debugFunction_walkNode(noNode);

        if( duplicatePathHit === true){
            return true;
        }

        // the paths below this node returned no duplicates
        return false;
    },

    // checks to see if this node has previously been visited
    // if it has it will be set to 1 in the array and we return false (exists)
    // if it hasnt we set it to 1 and return true (added)
    debugFunction_AddToVisited: function (nodeId) {

        if (visited[nodeId] === 1) {
            // node previously added - duplicate exists
            // console.log("Node was previously visited - duplicate detected");
            return false;
        }

        // was not found so add it as a visited node
        visited[nodeId] = 1;
        return true;
    },

    /** Gets the given album's review from Pitchfork.
     *  Calls the callbackfunction with the abstract of the review.
     */
    getAlbumRating: function (artistAndAlbum, callbackFunction, errorCallbackFunction) {
        // Scrape http://pitchfork.com/search/?query=
        // to find out the rating for the given album
        // Scrape tutorial here: https://www.sitepoint.com/web-scraping-in-node-js/
        var prefix = 'http://pitchfork.com';
        request({
            uri: prefix + '/search/?query=' + artistAndAlbum,
        }, function(error, response, body) {
            try{
                var resultObject = JSON.parse(body.split('window.App=')[1].split(';</script>')[0]);
                var review = resultObject.context.dispatcher.stores.SearchStore.results.albumreviews.items[0];
                // Cut out HTML tags from the abstract
                review.abstract = review.abstract.replace(/<\/?[^>]+(>|$)/g, "");
                request({
                    uri: prefix + review.site_url
                }, function(error, response, body) {
                    var $ = cheerio.load(body);
                    review.rating = $(".score").html();
                    callbackFunction(review);
                });
            } catch (error) {
                errorCallbackFunction();
            }
        });
    }
};<commit_msg>Include a LaunchRequest response<commit_after>var fs = require('fs');
var request = require("request");
var cheerio = require("cheerio");
var AlexaSkill = require('./AlexaSkill');

var APP_ID = 'amzn1.ask.skill.0fdc78be-c6a1-462b-9cfc-c8c1e01aeb11';

var intentHandlersUS = {
    "GetNewAlbumRatingIntent": function (intent, session, response) {
        handleAlbumRatingRequest(intent, response, 'US');
    },

    "AMAZON.HelpIntent": function (intent, session, response) {
        response.ask("You can make a request like 'Ask album rating about Lemonade by Beyonce!', or, you can say 'Exit!'... What can I help you with?", "What can I help you with?");
    },

    "AMAZON.StopIntent": function (intent, session, response) {
        var speechOutput = "Goodbye";
        this.emit(':tell', speechOutput);
    },

    "AMAZON.CancelIntent": function (intent, session, response) {
        var speechOutput = "Goodbye";
        this.emit(':tell', speechOutput);
    }
};

var intentHandlersUK = {
    "GetNewAlbumRatingIntent": function (intent, session, response) {
        handleAlbumRatingRequest(intent, response, 'UK');
    },

    "AMAZON.HelpIntent": function (intent, session, response) {
        response.ask("You can make a request like 'Ask album rating about Lemonade by Beyonce!', or, you can say 'Exit!'... What can I help you with?", "What can I help you with?");
    },

    "AMAZON.StopIntent": function (intent, session, response) {
        var speechOutput = "Goodbye";
        response.tell(speechOutput);
    },

    "AMAZON.CancelIntent": function (intent, session, response) {
        var speechOutput = "Goodbye";
        response.tell(speechOutput);
    }
};

var intentHandlersDE = {
    "GetNewAlbumRatingIntent": function (intent, session, response) {
        handleAlbumRatingRequest(intent,response, 'DE');
    },

    "AMAZON.HelpIntent": function (intent, session, response) {
        response.ask("You can make a request like 'Ask album rating about Lemonade by Beyonce!', or, you can say 'Exit!'... What can I help you with?", "What can I help you with?");
    },

    "AMAZON.StopIntent": function (intent, session, response) {
        var speechOutput = "Auf Wiedersehen!";
        response.tell(speechOutput);
    },

    "AMAZON.CancelIntent": function (intent, session, response) {
        var speechOutput = "Auf Wiedersehen!";
        response.tell(speechOutput);
    }
};

function handleAlbumRatingRequest(intent, response, language) {
    var album = intent.slots.Album.value? intent.slots.Album.value : '';
    var artist = intent.slots.Artist.value? intent.slots.Artist.value : '';
    var albumAndArtist = album + ' ' + artist;

    helper.getAlbumRating(albumAndArtist, function(albumReview) {
        var speechOutput = '';
        if(language !== 'DE'){
            speechOutput = "Pitchfork rated " + album + " by " + artist + " at " + albumReview.rating + ". They wrote: " + albumReview.abstract;
        } else {
            speechOutput = 'Pitchfork hat ' + album + " von " + artist + " mit " + albumReview.rating + "bewertet. In der Zusammenfassung heißt es: " + albumReview.abstract;
        }
        response.tell(speechOutput);
    }, function(errorFeedback){
        if(language !== 'DE'){
            response.tell('I couldn\'t find a result for ' + album + ' by ' + artist + '.');
        } else {
            response.tell('Ich konnte keine Ergebnisse zu ' + album + ' von ' + artist + ' finden.')
        }
    });
}

// Create the handler that responds to the Alexa Request.
exports.handler = function (event, context) {
    // Create an instance of the SpaceGeek skill.
    var skill = new AlexaSkill(APP_ID);

    AlexaSkill.prototype.eventHandlers.onLaunch = function (launchRequest, session, response) {
        response.tell('Which album can I help you with? Try it out like this: \'Ask album rating about Lemonade by Beyonce!\'');
    }

    var locale = event.request.locale;

    if (locale == 'en-GB'){
        skill.intentHandlers = intentHandlersUK;
    } else if (locale == 'de-DE') {
        skill.intentHandlers = intentHandlersDE;
    } else {
        skill.intentHandlers = intentHandlersUS;
    }

    skill.execute(event, context);
};

// --------------- Helper Functions  -----------------------

var helper = {

    // gives the user more information on their final choice
    giveDescription: function (context) {

        // get the speech for the child node
        var description = helper.getDescriptionForNode(context.attributes.currentNode);
        var message = description + ', ' + repeatWelcomeMessage;

        context.emit(':ask', message, message);
    },

    // logic to provide the responses to the yes or no responses to the main questions
    yesOrNo: function (context, reply) {

        // this is a question node so we need to see if the user picked yes or no
        var nextNodeId = helper.getNextNode(context.attributes.currentNode, reply);

        // error in node data
        if (nextNodeId == -1)
        {
            context.handler.state = states.STARTMODE;

            // the current node was not found in the nodes array
            // this is due to the current node in the nodes array having a yes / no node id for a node that does not exist
            context.emit(':tell', nodeNotFoundMessage, nodeNotFoundMessage);
        }

        // get the speech for the child node
        var message = helper.getSpeechForNode(nextNodeId);

        // have we made a decision
        if (helper.isAnswerNode(nextNodeId) === true) {

            // set the game state to description mode
            context.handler.state = states.DESCRIPTIONMODE;

            // append the play again prompt to the decision and speak it
            message = decisionMessage + ' ' + message + ' ,' + playAgainMessage;
        }

        // set the current node to next node we want to go to
        context.attributes.currentNode = nextNodeId;

        context.emit(':ask', message, message);
    },

    // gets the description for the given node id
    getDescriptionForNode: function (nodeId) {

        for (var i = 0; i < nodes.length; i++) {
            if (nodes[i].node == nodeId) {
                return nodes[i].description;
            }
        }
        return descriptionNotFoundMessage + nodeId;
    },

    // returns the speech for the provided node id
    getSpeechForNode: function (nodeId) {

        for (var i = 0; i < nodes.length; i++) {
            if (nodes[i].node == nodeId) {
                return nodes[i].message;
            }
        }
        return speechNotFoundMessage + nodeId;
    },

    // checks to see if this node is an choice node or a decision node
    isAnswerNode: function (nodeId) {

        for (var i = 0; i < nodes.length; i++) {
            if (nodes[i].node == nodeId) {
                if (nodes[i].yes === 0 && nodes[i].no === 0) {
                    return true;
                }
            }
        }
        return false;
    },

    // gets the next node to traverse to based on the yes no response
    getNextNode: function (nodeId, yesNo) {
        for (var i = 0; i < nodes.length; i++) {
            if (nodes[i].node == nodeId) {
                if (yesNo == "yes") {
                    return nodes[i].yes;
                }
                return nodes[i].no;
            }
        }
        // error condition, didnt find a matching node id. Cause will be a yes / no entry in the array but with no corrosponding array entry
        return -1;
    },

    // Recursively walks the node tree looking for nodes already visited
    // This method could be changed if you want to implement another type of checking mechanism
    // This should be run on debug builds only not production
    // returns false if node tree path does not contain any previously visited nodes, true if it finds one
    debugFunction_walkNode: function (nodeId) {

        // console.log("Walking node: " + nodeId);

        if( helper.isAnswerNode(nodeId) === true) {
            // found an answer node - this path to this node does not contain a previously visted node
            // so we will return without recursing further

            // console.log("Answer node found");
             return false;
        }

        // mark this question node as visited
        if( helper.debugFunction_AddToVisited(nodeId) === false)
        {
            // node was not added to the visited list as it already exists, this indicates a duplicate path in the tree
            return true;
        }

        // console.log("Recursing yes path");
        var yesNode = helper.getNextNode(nodeId, "yes");
        var duplicatePathHit = helper.debugFunction_walkNode(yesNode);

        if( duplicatePathHit === true){
            return true;
        }

        // console.log("Recursing no");
        var noNode = helper.getNextNode(nodeId, "no");
        duplicatePathHit = helper.debugFunction_walkNode(noNode);

        if( duplicatePathHit === true){
            return true;
        }

        // the paths below this node returned no duplicates
        return false;
    },

    // checks to see if this node has previously been visited
    // if it has it will be set to 1 in the array and we return false (exists)
    // if it hasnt we set it to 1 and return true (added)
    debugFunction_AddToVisited: function (nodeId) {

        if (visited[nodeId] === 1) {
            // node previously added - duplicate exists
            // console.log("Node was previously visited - duplicate detected");
            return false;
        }

        // was not found so add it as a visited node
        visited[nodeId] = 1;
        return true;
    },

    /** Gets the given album's review from Pitchfork.
     *  Calls the callbackfunction with the abstract of the review.
     */
    getAlbumRating: function (artistAndAlbum, callbackFunction, errorCallbackFunction) {
        // Scrape http://pitchfork.com/search/?query=
        // to find out the rating for the given album
        // Scrape tutorial here: https://www.sitepoint.com/web-scraping-in-node-js/
        var prefix = 'http://pitchfork.com';
        request({
            uri: prefix + '/search/?query=' + artistAndAlbum,
        }, function(error, response, body) {
            try{
                var resultObject = JSON.parse(body.split('window.App=')[1].split(';</script>')[0]);
                var review = resultObject.context.dispatcher.stores.SearchStore.results.albumreviews.items[0];
                // Cut out HTML tags from the abstract
                review.abstract = review.abstract.replace(/<\/?[^>]+(>|$)/g, "");
                request({
                    uri: prefix + review.site_url
                }, function(error, response, body) {
                    var $ = cheerio.load(body);
                    review.rating = $(".score").html();
                    callbackFunction(review);
                });
            } catch (error) {
                errorCallbackFunction();
            }
        });
    }
};<|endoftext|>