<commit_before>'use strict'

const Promise = require('bluebird')
const t = require('../lib/translator')
const Logger = require('../lib/logger')
const languages = require('../lib/lang')
const mixpanel = require('../lib/mixpanel')
const state = require('../lib/state')
const repo = require('../db/repo')
const config = require('../config')
const _ = require('lodash')

const maxTextReplyLength = 320

const responseType = {
  help: '#help',
  getStarted: '#getstarted',
  reset: '#reset',
  switch: '#switch',
  list: '#list'
}

const helpQuickReply = {
  'content_type': 'text',
  'title': 'Need help?',
  'payload': '#help'
}

const examples = [
  'English', 'German', 'Italian', 'Korean', 'Dutch', 'Japanese', 'Hindi', 'Spanish', 'French', 'Indonesian', 'Russian', 'Chinese', 'Greek'
]

class MessageHandler {

  constructor(bot) {
    this.bot = bot
  }

  // next middleware
  next(sender) {
    let next = this.bot
    const profile = state.get(sender.id)
    if (profile !== undefined) {
      // pseudo middleware handler
      next = {
        getProfile: (id, cb) => {
          cb(null, profile)
        }
      }
    }
    return next
  }

  // typing sends a typing response to the recipient
  typing(sender) {
    this.bot.setTyping(sender.id, true)
  }

  // handleMessage is our main handler
  handleMessage(payload, reply) {

    const { sender, message, postback } = payload

    // we don't care about handling our own responses
    if (sender.id === config.fb_page_id) return

    return new Promise((resolve) => {

      const next = this.next(sender)
      next.getProfile(sender.id, (err, profile) => {
        if (err) return Logger.log(`getProfileError: ${JSON.stringify(err)}`)
        // cache the user for subsequent requests
        state.set(sender.id, profile)

        repo(sender.id).get().then((response, err) => {
          if (err) Logger.log(`getAsyncError: ${JSON.stringify(err)}`)

          const { context } = response

          // check for postbacks
          if (postback && postback.payload) {
            return resolve(this.handlePostBack(context, postback, profile, sender, reply))
          }
          // check if a quick reply (comes in as a message)
          if (message && message.quick_reply && message.quick_reply.payload) {
            return resolve(this.handlePostBack(context, message.quick_reply, profile, sender, reply))
          }

          // not a postback and not a text message, return
          if (!message.text) return resolve(false)

          if (_.includes(message.text.toLowerCase(), 'help') && !context) {
            return resolve(this.handleHelp(context, profile, sender, reply))
          }
          // reset or switch command
          if (message.text === responseType.reset || message.text === responseType.switch) {
            return resolve(this.handlePostBack(context, {payload: message.text}, profile, sender, reply))
          }
          // no context
          if (!context) {
            return resolve(this.handleNoContext(sender, profile, message, reply))
          }
          // we have context and the user has sent a text message, before translation check if this is
          // a possible direct change command: {lang} to {lang}
          if (isPossibleChangeCommand(message.text)) {
            const context = getContextFromMessage(message.text, true)
            if (context.hasTwo && context.from !== context.to) {
              return (resolve(this.handleSetContext(context.code, context.from, context.to, sender, null, reply)))
            }
          }
          // we made it! translate the message
          return resolve(this.handleTranslation(context, sender, message, reply))
        })
      })
    })
  }

  // handlePostBack
  handlePostBack(context, postback, profile, sender, reply) {
    Logger.log('handlePostBack')
    if (postback.payload === responseType.getStarted) {
      return this.handleGetStarted(sender, profile, reply)
    }
    if (postback.payload === responseType.help) {
      return this.handleHelp(context, profile, sender, reply)
    }
    if (postback.payload === responseType.reset) {
      return this.handleReset(sender, reply)
    }
    if (postback.payload === responseType.switch) {
      return this.handleSwitch(context, sender, reply)
    }
    if (postback.payload === responseType.list) {
      return this.handleShowAllLanguages(sender, reply)
    }
  }

  // handleGetStarted
  handleGetStarted(sender, profile, reply) {
    Logger.log('handleGetStarted')
    reply({
      text: `Hola ${profile && profile.first_name}! Start by telling me what languages to translate for you. Say something like ${getSmartExample(profile)}\n\nAsk me for "help" at any time`
    }, () => {
      mixpanel.setPerson(sender, profile)
      mixpanel.track('I click to get started', sender)
    })
  }

  // handleHelp
  handleHelp(context, profile, sender, reply) {
    Logger.log('handleHelp')
    let text = `Hola. I see you've asked for some help... \n\nTell me what languages to translate by saying something like ${getSmartExample(profile)}`
    let options = [
      {
        'type': 'postback',
        'title': 'Show all languages',
        'payload': responseType.list
      }
    ]

    if (context) {
      context = getContextFromCode(context)
      text = `Hola again. I see you've asked for some help...\n\nI'm currently translating everything you say from ${context.from} to ${context.to}\n\n`
      options.unshift(
        {
          'type': 'postback',
          'title': 'Reset',
          'payload': responseType.reset
        },
        {
          'type': 'postback',
          'title': `${context.to} to ${context.from}`,
          'payload': responseType.switch
        }
      )
    }

    return reply({
      'attachment': {
        'type': 'template',
        'payload': {
          'template_type': 'button',
          'text': text,
          'buttons': options
        }
      }
    }, () => {
      mixpanel.track('I ask for help', sender)
    })
  }

  // handleShowAllLanguages
  // due to FBs payload size limit we need to chunk the list into seprate bits
  handleShowAllLanguages(sender, reply) {
    const list = getAllLanguageNames()
    const first = list.splice(0, list.length / 3)
    const second = list.splice(0, list.length / 2)
    reply({text: `OK, here goes...\n\n${first.toString().replace(/,/g, ', ')}`}, () => {
      reply({text: `${second.toString().replace(/,/g, ', ')}`}, () => {
        reply({text: `${list.toString().replace(/,/g, ', ')}\n\n *GASP*`})
      })
    })
  }

  // handleReset
  handleReset(sender, reply) {
    repo(sender.id).set('').then((err) => {
      if (err) Logger.log(err)
      return reply({
        text: 'OK, what should I translate for you next?'
      }, () => {
        mixpanel.track('I reset context', sender)
      })
    })
  }

  // handleSwitch
  handleSwitch(context, sender, reply) {
    context = switchContext(getContextFromCode(context))
    return this.handleSetContext(context.code, context.from, context.to, sender, null, reply)
  }

  // handleNoContext
  handleNoContext(sender, profile, message, reply) {
    Logger.log('handleNoContext')
    const context = getContextFromMessage(message.text)
    if (context.hasTwo && context.from !== context.to) {
      return this.handleSetContext(context.code, context.from, context.to, sender, message, reply)
    }
    if (this.handleGeneralResponse(sender, profile, message, reply) !== false) {
      return
    }

    let text = 'Oops, I didn\'t quite catch that. Ask me for "help" at anytime'
    if (context.hasOne) text = `I only caught ${context.from} in there. Try again, or ask me for "help"`
    return reply({
      text: text
    }, () => {
      mixpanel.track('I incorrectly set context', sender, message)
    })
  }

  // handleGeneralResponse such as hello, goodbye etc
  handleGeneralResponse(sender, profile, message, reply) {
    if (_.includes(['goodbye', 'bye', 'adios', 'ciao', 'seeya', 'see you', 'later'], message.text.toLowerCase())) {
      return reply({
        text: `Adiós ${profile.gender === 'male' ? 'muchacho' : 'muchacha'}`
      }, () => {
        mixpanel.track('I say goodbye without context', sender, message)
      })
    }
    if (_.includes(['hello', 'hi', 'howdy', 'hallo', 'yo', 'hey', 'sup', 'hiya'], message.text.toLowerCase())) {
      return reply({
        text: getRandom([`Hi ${profile.first_name}!`, 'Hello', '¡Hola!', `Hey ${profile.first_name}!`, 'Oh hey there!', 'Hallo', 'Howdy', 'Oh Hiiiiii'])
      }, () => {
        mixpanel.track('I say hello without context', sender, message)
      })
    }
    if (_.includes(message.text.toLowerCase(), 'hmm')) {
      return reply({ text: 'hmmmm...' })
    }
    return false
  }

  // handleSetContext
  handleSetContext(code, from, to, sender, message, reply) {
    Logger.log('handleSetContext')
    repo(sender.id).set(code).then((err) => {
      if (err) Logger.log(err)
      return reply({
        text: `${from} to ${to}. Got it! Now go ahead and tell me what to say.`
      }, () => {
        mixpanel.track('I set context', sender, message)
      })
    })
  }

  // handleTranslation
  handleTranslation(context, sender, message, reply) {
    let response = {}
    this.typing(sender)
    t.translate(message.text, context).
      then((result) => {
        if (result.length > maxTextReplyLength) {
          return reply({text: 'I\'m sorry, I can\'t translate all of that in one go. Try again with a smaller message'})
        }
        response.text = result
        // check if the user actually wants help by passing a quick reply button with the translated text
        if (_.includes(message.text.toLowerCase(), 'help')) {
          response.quick_replies = [helpQuickReply]
        }
        reply(response, (err) => {
          if (err) Logger.log(err)
          mixpanel.track('I send a message to be translated', sender, message)
        })
      }).
      catch((err) => {
        if (err) Logger.log(err)
        reply({ text: 'Oh no, something has gone wrong. Please try that again' })
      })
  }

  // _privates
  // expose these for unit testing
  _privates() {
    return {
      getContextFromMessage: getContextFromMessage,
      getContextFromCode: getContextFromCode,
      switchContext: switchContext,
      getLanguageName: getLanguageName,
      getAllLanguageNames: getAllLanguageNames,
      getLanguageNameLocale: getLanguageNameLocale
    }
  }
}

// private methods
// getContextFromMessage
function getContextFromMessage(message, strict) {
  const ctxMatches = getContextMatches(message, strict)
  const { hasTwo, hasOne, hasNone } = ctxMatches
  let code, from, to
  if (hasOne || hasTwo) {
    from = _.capitalize(ctxMatches.matches[0].name)
  }
  if (hasTwo) {
    code = `${ctxMatches.matches[0].code}:${ctxMatches.matches[1].code}`
    to = _.capitalize(ctxMatches.matches[1].name)
  }
  return { code, from, to, hasTwo, hasOne, hasNone }
}

// getContextFromCode
function getContextFromCode(code) {
  const codes = code.split(':')
  return {
    code,
    from: getLanguageName(codes[0]),
    to: getLanguageName(codes[1]),
    hasTwo: true
  }
}

// getContext
function getContextMatches(message, strict) {
  let matches = []
  const words = message.split(' ')
  if (!strict || (strict && words.length === 3)) {
    words.forEach((word) => {
      languages.filter(item => item.name === _.lowerCase(word)).map((lang) => {
        if (matches.length === 2) return
        matches.push(lang)
      })
    })
  }
  return {
    hasTwo: matches.length === 2,
    hasOne: matches.length === 1,
    hasNone: matches.length === 0,
    matches: matches
  }
}

// switchContext
function switchContext(context) {
  const values = context.code.split(':')
  const from = context.to
  const to = context.from

  context.code = `${values[1]}:${values[0]}`
  context.from = from
  context.to = to

  return context
}

// getLanguageName
function getLanguageName(code) {
  const name = languages.filter(item => item.code === code).map((lang) => {
    return lang.name
  })
  if (name && name.length) return _.capitalize(name[0])
}

// listAllLanguages
function getAllLanguageNames() {
  let list = []
  languages.forEach(function(lang) {
    list.push(_.capitalize(lang.name))
  })
  return list
}

// getLanguageNameLocale
function getLanguageNameLocale(profile) {
  if (profile && !profile.locale && profile.locale.indexOf('_') === -1) return ''
  const code = profile.locale.split('_')[0]
  return getLanguageName(code)
}

// getRandom
function getRandom(responses) {
  return responses[Math.floor(Math.random()*responses.length)]
}

// getSmartExample
function getSmartExample(profile) {
  const locale = getLanguageNameLocale(profile)
  let shuffled = shuffleArray(_.clone(examples))
  if (!locale) return `"${shuffled[0]} to ${shuffled[1]}"`

  shuffled = _.remove(shuffled, (n) => { return n !== locale })
  return `"${locale} to ${shuffled[0]}"`
}

// shuffleArray
function shuffleArray(array) {
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1))
    const temp = array[i]
    array[i] = array[j]
    array[j] = temp
  }
  return array
}

// isPossibleChangeCommand
function isPossibleChangeCommand(message) {
  const words = message.split(' ')
  return words.length === 3 && words[1] === 'to'
}

module.exports = MessageHandler
<commit_msg>return undefined<commit_after>'use strict'

const Promise = require('bluebird')
const t = require('../lib/translator')
const Logger = require('../lib/logger')
const languages = require('../lib/lang')
const mixpanel = require('../lib/mixpanel')
const state = require('../lib/state')
const repo = require('../db/repo')
const config = require('../config')
const _ = require('lodash')

const maxTextReplyLength = 320

const responseType = {
  help: '#help',
  getStarted: '#getstarted',
  reset: '#reset',
  switch: '#switch',
  list: '#list'
}

const helpQuickReply = {
  'content_type': 'text',
  'title': 'Need help?',
  'payload': '#help'
}

const examples = [
  'English', 'German', 'Italian', 'Korean', 'Dutch', 'Japanese', 'Hindi', 'Spanish', 'French', 'Indonesian', 'Russian', 'Chinese', 'Greek'
]

class MessageHandler {

  constructor(bot) {
    this.bot = bot
  }

  // next middleware
  next(sender) {
    let next = this.bot
    const profile = state.get(sender.id)
    if (profile !== undefined) {
      // pseudo middleware handler
      next = {
        getProfile: (id, cb) => {
          cb(null, profile)
        }
      }
    }
    return next
  }

  // typing sends a typing response to the recipient
  typing(sender) {
    this.bot.setTyping(sender.id, true)
  }

  // handleMessage is our main handler
  handleMessage(payload, reply) {

    const { sender, message, postback } = payload

    // we don't care about handling our own responses
    if (sender.id === config.fb_page_id) return

    return new Promise((resolve) => {

      const next = this.next(sender)
      next.getProfile(sender.id, (err, profile) => {
        if (err) return Logger.log(`getProfileError: ${JSON.stringify(err)}`)
        // cache the user for subsequent requests
        state.set(sender.id, profile)

        repo(sender.id).get().then((response, err) => {
          if (err) Logger.log(`getAsyncError: ${JSON.stringify(err)}`)

          const { context } = response

          // check for postbacks
          if (postback && postback.payload) {
            return resolve(this.handlePostBack(context, postback, profile, sender, reply))
          }
          // check if a quick reply (comes in as a message)
          if (message && message.quick_reply && message.quick_reply.payload) {
            return resolve(this.handlePostBack(context, message.quick_reply, profile, sender, reply))
          }

          // not a postback and not a text message, return
          if (!message.text) return resolve(false)

          if (_.includes(message.text.toLowerCase(), 'help') && !context) {
            return resolve(this.handleHelp(context, profile, sender, reply))
          }
          // reset or switch command
          if (message.text === responseType.reset || message.text === responseType.switch) {
            return resolve(this.handlePostBack(context, {payload: message.text}, profile, sender, reply))
          }
          // no context
          if (!context) {
            return resolve(this.handleNoContext(sender, profile, message, reply))
          }
          // we have context and the user has sent a text message, before translation check if this is
          // a possible direct change command: {lang} to {lang}
          if (isPossibleChangeCommand(message.text)) {
            const context = getContextFromMessage(message.text, true)
            if (context.hasTwo && context.from !== context.to) {
              return (resolve(this.handleSetContext(context.code, context.from, context.to, sender, null, reply)))
            }
          }
          // we made it! translate the message
          return resolve(this.handleTranslation(context, sender, message, reply))
        })
      })
    })
  }

  // handlePostBack
  handlePostBack(context, postback, profile, sender, reply) {
    Logger.log('handlePostBack')
    if (postback.payload === responseType.getStarted) {
      return this.handleGetStarted(sender, profile, reply)
    }
    if (postback.payload === responseType.help) {
      return this.handleHelp(context, profile, sender, reply)
    }
    if (postback.payload === responseType.reset) {
      return this.handleReset(sender, reply)
    }
    if (postback.payload === responseType.switch) {
      return this.handleSwitch(context, sender, reply)
    }
    if (postback.payload === responseType.list) {
      return this.handleShowAllLanguages(sender, reply)
    }
  }

  // handleGetStarted
  handleGetStarted(sender, profile, reply) {
    Logger.log('handleGetStarted')
    reply({
      text: `Hola ${profile && profile.first_name}! Start by telling me what languages to translate for you. Say something like ${getSmartExample(profile)}\n\nAsk me for "help" at any time`
    }, () => {
      mixpanel.setPerson(sender, profile)
      mixpanel.track('I click to get started', sender)
    })
  }

  // handleHelp
  handleHelp(context, profile, sender, reply) {
    Logger.log('handleHelp')
    let text = `Hola. I see you've asked for some help... \n\nTell me what languages to translate by saying something like ${getSmartExample(profile)}`
    let options = [
      {
        'type': 'postback',
        'title': 'Show all languages',
        'payload': responseType.list
      }
    ]

    if (context) {
      context = getContextFromCode(context)
      text = `Hola again. I see you've asked for some help...\n\nI'm currently translating everything you say from ${context.from} to ${context.to}\n\n`
      options.unshift(
        {
          'type': 'postback',
          'title': 'Reset',
          'payload': responseType.reset
        },
        {
          'type': 'postback',
          'title': `${context.to} to ${context.from}`,
          'payload': responseType.switch
        }
      )
    }

    return reply({
      'attachment': {
        'type': 'template',
        'payload': {
          'template_type': 'button',
          'text': text,
          'buttons': options
        }
      }
    }, () => {
      mixpanel.track('I ask for help', sender)
    })
  }

  // handleShowAllLanguages
  // due to FBs payload size limit we need to chunk the list into seprate bits
  handleShowAllLanguages(sender, reply) {
    const list = getAllLanguageNames()
    const first = list.splice(0, list.length / 3)
    const second = list.splice(0, list.length / 2)
    reply({text: `OK, here goes...\n\n${first.toString().replace(/,/g, ', ')}`}, () => {
      reply({text: `${second.toString().replace(/,/g, ', ')}`}, () => {
        reply({text: `${list.toString().replace(/,/g, ', ')}\n\n *GASP*`})
      })
    })
  }

  // handleReset
  handleReset(sender, reply) {
    repo(sender.id).set('').then((err) => {
      if (err) Logger.log(err)
      return reply({
        text: 'OK, what should I translate for you next?'
      }, () => {
        mixpanel.track('I reset context', sender)
      })
    })
  }

  // handleSwitch
  handleSwitch(context, sender, reply) {
    context = switchContext(getContextFromCode(context))
    return this.handleSetContext(context.code, context.from, context.to, sender, null, reply)
  }

  // handleNoContext
  handleNoContext(sender, profile, message, reply) {
    Logger.log('handleNoContext')
    const context = getContextFromMessage(message.text)
    if (context.hasTwo && context.from !== context.to) {
      return this.handleSetContext(context.code, context.from, context.to, sender, message, reply)
    }
    if (this.handleGeneralResponse(sender, profile, message, reply) !== false) {
      return
    }

    let text = 'Oops, I didn\'t quite catch that. Ask me for "help" at anytime'
    if (context.hasOne) text = `I only caught ${context.from} in there. Try again, or ask me for "help"`
    return reply({
      text: text
    }, () => {
      mixpanel.track('I incorrectly set context', sender, message)
    })
  }

  // handleGeneralResponse such as hello, goodbye etc
  handleGeneralResponse(sender, profile, message, reply) {
    if (_.includes(['goodbye', 'bye', 'adios', 'ciao', 'seeya', 'see you', 'later'], message.text.toLowerCase())) {
      return reply({
        text: `Adiós ${profile.gender === 'male' ? 'muchacho' : 'muchacha'}`
      }, () => {
        mixpanel.track('I say goodbye without context', sender, message)
      })
    }
    if (_.includes(['hello', 'hi', 'howdy', 'hallo', 'yo', 'hey', 'sup', 'hiya'], message.text.toLowerCase())) {
      return reply({
        text: getRandom([`Hi ${profile.first_name}!`, 'Hello', '¡Hola!', `Hey ${profile.first_name}!`, 'Oh hey there!', 'Hallo', 'Howdy', 'Oh Hiiiiii'])
      }, () => {
        mixpanel.track('I say hello without context', sender, message)
      })
    }
    if (_.includes(message.text.toLowerCase(), 'hmm')) {
      return reply({ text: 'hmmmm...' })
    }
    return false
  }

  // handleSetContext
  handleSetContext(code, from, to, sender, message, reply) {
    Logger.log('handleSetContext')
    repo(sender.id).set(code).then((err) => {
      if (err) Logger.log(err)
      return reply({
        text: `${from} to ${to}. Got it! Now go ahead and tell me what to say.`
      }, () => {
        mixpanel.track('I set context', sender, message)
      })
    })
  }

  // handleTranslation
  handleTranslation(context, sender, message, reply) {
    let response = {}
    this.typing(sender)
    t.translate(message.text, context).
      then((result) => {
        if (result.length > maxTextReplyLength) {
          return reply({text: 'I\'m sorry, I can\'t translate all of that in one go. Try again with a smaller message'})
        }
        response.text = result
        // check if the user actually wants help by passing a quick reply button with the translated text
        if (_.includes(message.text.toLowerCase(), 'help')) {
          response.quick_replies = [helpQuickReply]
        }
        reply(response, (err) => {
          if (err) Logger.log(err)
          mixpanel.track('I send a message to be translated', sender, message)
        })
      }).
      catch((err) => {
        if (err) Logger.log(err)
        reply({ text: 'Oh no, something has gone wrong. Please try that again' })
      })
  }

  // _privates
  // expose these for unit testing
  _privates() {
    return {
      getContextFromMessage: getContextFromMessage,
      getContextFromCode: getContextFromCode,
      switchContext: switchContext,
      getLanguageName: getLanguageName,
      getAllLanguageNames: getAllLanguageNames,
      getLanguageNameLocale: getLanguageNameLocale
    }
  }
}

// private methods
// getContextFromMessage
function getContextFromMessage(message, strict) {
  const ctxMatches = getContextMatches(message, strict)
  const { hasTwo, hasOne, hasNone } = ctxMatches
  let code, from, to
  if (hasOne || hasTwo) {
    from = _.capitalize(ctxMatches.matches[0].name)
  }
  if (hasTwo) {
    code = `${ctxMatches.matches[0].code}:${ctxMatches.matches[1].code}`
    to = _.capitalize(ctxMatches.matches[1].name)
  }
  return { code, from, to, hasTwo, hasOne, hasNone }
}

// getContextFromCode
function getContextFromCode(code) {
  const codes = code.split(':')
  return {
    code,
    from: getLanguageName(codes[0]),
    to: getLanguageName(codes[1]),
    hasTwo: true
  }
}

// getContext
function getContextMatches(message, strict) {
  let matches = []
  const words = message.split(' ')
  if (!strict || (strict && words.length === 3)) {
    words.forEach((word) => {
      languages.filter(item => item.name === _.lowerCase(word)).map((lang) => {
        if (matches.length === 2) return
        matches.push(lang)
      })
    })
  }
  return {
    hasTwo: matches.length === 2,
    hasOne: matches.length === 1,
    hasNone: matches.length === 0,
    matches: matches
  }
}

// switchContext
function switchContext(context) {
  const values = context.code.split(':')
  const from = context.to
  const to = context.from

  context.code = `${values[1]}:${values[0]}`
  context.from = from
  context.to = to

  return context
}

// getLanguageName
function getLanguageName(code) {
  const name = languages.filter(item => item.code === code).map((lang) => {
    return lang.name
  })
  if (name && name.length) return _.capitalize(name[0])
}

// listAllLanguages
function getAllLanguageNames() {
  let list = []
  languages.forEach(function(lang) {
    list.push(_.capitalize(lang.name))
  })
  return list
}

// getLanguageNameLocale
function getLanguageNameLocale(profile) {
  if (profile && !profile.locale && profile.locale.indexOf('_') === -1) return
  const code = profile.locale.split('_')[0]
  return getLanguageName(code)
}

// getRandom
function getRandom(responses) {
  return responses[Math.floor(Math.random()*responses.length)]
}

// getSmartExample
function getSmartExample(profile) {
  let shuffled = shuffleArray(_.clone(examples))
  const locale = getLanguageNameLocale(profile)
  if (!locale) return `"${shuffled[0]} to ${shuffled[1]}"`

  shuffled = _.remove(shuffled, (n) => { return n !== locale })
  return `"${locale} to ${shuffled[0]}"`
}

// shuffleArray
function shuffleArray(array) {
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1))
    const temp = array[i]
    array[i] = array[j]
    array[j] = temp
  }
  return array
}

// isPossibleChangeCommand
function isPossibleChangeCommand(message) {
  const words = message.split(' ')
  return words.length === 3 && words[1] === 'to'
}

module.exports = MessageHandler
<|endoftext|>


<commit_before>'use strict'

export const search_route_map = {
  path: '/search',
  methods: {
    get: (request, reply) => {
      reply().code(204)
    },
    post: {
      handler: (request, reply) => {
        const index = request.server.app.index
        reply(index.search(request.payload.q, request.payload.config))
      },
      config: {
        auth: 'simple'
      }
    }
  }
}
<commit_msg>add cors option<commit_after>'use strict'

export const search_route_map = {
  path: '/search',
  methods: {
    get: (request, reply) => {
      reply().code(204)
    },
    post: {
      handler: (request, reply) => {
        const index = request.server.app.index
        console.log(request.payload)
        reply(index.search(request.payload.q, request.payload.config))
      },
      config: {
        auth: 'simple',
        cors: true
      }
    }
  }
}
<|endoftext|>


<commit_before>/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2018 Photon Storm Ltd.
 * @license      {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
 */

/**
 * Global consts.
 *
 * @ignore
 */

var CONST = {

    /**
     * Phaser Release Version
     *
     * @name Phaser.VERSION
     * @readonly
     * @type {string}
     * @since 3.0.0
     */
    VERSION: '3.16.0 Beta 3',

    BlendModes: require('./renderer/BlendModes'),

    ScaleModes: require('./renderer/ScaleModes'),

    /**
     * AUTO Detect Renderer.
     *
     * @name Phaser.AUTO
     * @readonly
     * @type {integer}
     * @since 3.0.0
     */
    AUTO: 0,

    /**
     * Canvas Renderer.
     *
     * @name Phaser.CANVAS
     * @readonly
     * @type {integer}
     * @since 3.0.0
     */
    CANVAS: 1,

    /**
     * WebGL Renderer.
     *
     * @name Phaser.WEBGL
     * @readonly
     * @type {integer}
     * @since 3.0.0
     */
    WEBGL: 2,

    /**
     * Headless Renderer.
     *
     * @name Phaser.HEADLESS
     * @readonly
     * @type {integer}
     * @since 3.0.0
     */
    HEADLESS: 3,

    /**
     * In Phaser the value -1 means 'forever' in lots of cases, this const allows you to use it instead
     * to help you remember what the value is doing in your code.
     *
     * @name Phaser.FOREVER
     * @readonly
     * @type {integer}
     * @since 3.0.0
     */
    FOREVER: -1,

    /**
     * Direction constant.
     *
     * @name Phaser.NONE
     * @readonly
     * @type {integer}
     * @since 3.0.0
     */
    NONE: 4,

    /**
     * Direction constant.
     *
     * @name Phaser.UP
     * @readonly
     * @type {integer}
     * @since 3.0.0
     */
    UP: 5,

    /**
     * Direction constant.
     *
     * @name Phaser.DOWN
     * @readonly
     * @type {integer}
     * @since 3.0.0
     */
    DOWN: 6,

    /**
     * Direction constant.
     *
     * @name Phaser.LEFT
     * @readonly
     * @type {integer}
     * @since 3.0.0
     */
    LEFT: 7,

    /**
     * Direction constant.
     *
     * @name Phaser.RIGHT
     * @readonly
     * @type {integer}
     * @since 3.0.0
     */
    RIGHT: 8

};

module.exports = CONST;
<commit_msg>Beta 4<commit_after>/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2018 Photon Storm Ltd.
 * @license      {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
 */

/**
 * Global consts.
 *
 * @ignore
 */

var CONST = {

    /**
     * Phaser Release Version
     *
     * @name Phaser.VERSION
     * @readonly
     * @type {string}
     * @since 3.0.0
     */
    VERSION: '3.16.0 Beta 4',

    BlendModes: require('./renderer/BlendModes'),

    ScaleModes: require('./renderer/ScaleModes'),

    /**
     * AUTO Detect Renderer.
     *
     * @name Phaser.AUTO
     * @readonly
     * @type {integer}
     * @since 3.0.0
     */
    AUTO: 0,

    /**
     * Canvas Renderer.
     *
     * @name Phaser.CANVAS
     * @readonly
     * @type {integer}
     * @since 3.0.0
     */
    CANVAS: 1,

    /**
     * WebGL Renderer.
     *
     * @name Phaser.WEBGL
     * @readonly
     * @type {integer}
     * @since 3.0.0
     */
    WEBGL: 2,

    /**
     * Headless Renderer.
     *
     * @name Phaser.HEADLESS
     * @readonly
     * @type {integer}
     * @since 3.0.0
     */
    HEADLESS: 3,

    /**
     * In Phaser the value -1 means 'forever' in lots of cases, this const allows you to use it instead
     * to help you remember what the value is doing in your code.
     *
     * @name Phaser.FOREVER
     * @readonly
     * @type {integer}
     * @since 3.0.0
     */
    FOREVER: -1,

    /**
     * Direction constant.
     *
     * @name Phaser.NONE
     * @readonly
     * @type {integer}
     * @since 3.0.0
     */
    NONE: 4,

    /**
     * Direction constant.
     *
     * @name Phaser.UP
     * @readonly
     * @type {integer}
     * @since 3.0.0
     */
    UP: 5,

    /**
     * Direction constant.
     *
     * @name Phaser.DOWN
     * @readonly
     * @type {integer}
     * @since 3.0.0
     */
    DOWN: 6,

    /**
     * Direction constant.
     *
     * @name Phaser.LEFT
     * @readonly
     * @type {integer}
     * @since 3.0.0
     */
    LEFT: 7,

    /**
     * Direction constant.
     *
     * @name Phaser.RIGHT
     * @readonly
     * @type {integer}
     * @since 3.0.0
     */
    RIGHT: 8

};

module.exports = CONST;
<|endoftext|>


<commit_before>import safeEval from 'safe-eval';
import dir from 'node-dir';
import path from 'path';
import {ipcRenderer} from 'electron';
const babel = require('babel-core');
const babelOptions = {
  presets: ['es2015']
}

let scriptDirectory = process.env.script;

let options = {
  match: /.js$/,
  exclude: /^\./,
  recursive: false
}

if (path.isAbsolute(scriptDirectory)) {
  dir.readFiles(scriptDirectory, options, parseFile, success);
} else {
  throw new Error('Path for user script is not absolute.');
}

function parseFile (err, content, next) {
  if(err)
    throw err;

  let code;

  try {
    code = babel.transform(content, babelOptions).code;

  } catch(e) {
    if(e.name === 'TypeError') {
      // TODO: Handle syntax error and show it to the user
      console.log(e);
    } else {
      throw e;
    }
  } finally {
    ipcRenderer.send('code', {code});
  }

  next();
}

function success (err, files) {
  if(err)
    throw err;
  console.log("Finished reading all files!");
}<commit_msg>Add the current working directory for the user scripts to the main process. Preparation for possible handling of require<commit_after>import safeEval from 'safe-eval';
import dir from 'node-dir';
import path from 'path';
import {ipcRenderer} from 'electron';
const babel = require('babel-core');
const babelOptions = {
  presets: ['es2015']
}

let scriptDirectory = process.env.script;

let options = {
  match: /.js$/,
  exclude: /^\./,
  recursive: false
}

if (path.isAbsolute(scriptDirectory)) {
  dir.readFiles(scriptDirectory, options, parseFile, success);
} else {
  throw new Error('Path for user script is not absolute.');
}

function parseFile (err, content, next) {
  if(err)
    throw err;

  let code;

  try {
    code = babel.transform(content, babelOptions).code;

  } catch(e) {
    if(e.name === 'TypeError') {
      // TODO: Handle syntax error and show it to the user
      console.log(e);
    } else {
      throw e;
    }
  } finally {
    ipcRenderer.send('code', {code, dir: scriptDirectory});
  }

  next();
}

function success (err, files) {
  if(err)
    throw err;
  console.log("Finished reading all files!");
}<|endoftext|>


<commit_before>/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 Oskar Homburg
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
/*global require, module, exports */
// Polyfill and globals
if (Math.clamp === undefined) {
    Math.clamp = function (n, min, max) {
        return Math.min(Math.max(n, min), max);
    };
}

if (Math.HALF_PI === undefined) {
    Math.HALF_PI = Math.PI / 2;
}

// Thanks to Stackoverflow user fearphage
if (!String.format) {
    String.format = function (format) {
        var args = Array.prototype.slice.call(arguments, 1);
        return format.replace(/\{(\d+)\}/g, function (match, number) {
            return args[number] !== undefined ? args[number] : match;
        });
    };
}

var Clock = require('../common/clock'),
    settings = require('../common/settings'),
    controls = require('../client/controls'),
    simulator = require('../common/simulator').make(),
    commands = require('../common/commands'),
    level = require('../client/level'),
// Load rcon after level to avoid issues with cyclic deps
    rcon = require('../client/rcon'),
    scenemgr = require('../client/scene-manager'),
    protocol = require('../client/protocol'),
    arena = require('../common/arena'),
    client = require('../client/client'),
    console = require('../dom/console');

// Entry point
function entrypoint() {
    var display = require('../client/display');
    console.log("Playing Arena version {0}", arena.version);
    if (arena.debug) {
        console.warn("Debug mode is enabled");
        window.debugging = true;
    }

    function update(time) {
        controls.update(time);
        simulator.update(time);
        scenemgr.copyWorldToScene();
    }

    settings.api.init();
    scenemgr.init(display.scene, simulator);

    controls.firstPersonCam(display.camera);
    controls.sceneObj.position.set(0, 2, 0);
    controls.physBody.position.set(0, 2, 0);
    controls.physBody.linearDamping = 0.95;
    display.scene.add(controls.sceneObj);
    simulator.add(controls.physBody, 0);

    protocol.clientInterface = {
        spawnFromDesc: level.spawnFromDesc
    };

    // Add command shorthand
    console.executeFn = window.c = function (str) {
        return commands.execute(str, 'cl');
    };

    commands.register(level.commands);
    commands.register(display.commands);
    commands.register(controls.commands);
    commands.register(rcon.commands);
    commands.register(client.commands);

    display.render();
    protocol.simulator = simulator;
    Clock.startNew(16, update);
}

if (document.readyState === 'interactive') {
    entrypoint();
} else {
    document.addEventListener('DOMContentLoaded', entrypoint);
}<commit_msg>Move non-DOM code out of entrypoint()<commit_after>/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 Oskar Homburg
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
/*global require, module, exports */
// Polyfill and globals
if (Math.clamp === undefined) {
    Math.clamp = function (n, min, max) {
        return Math.min(Math.max(n, min), max);
    };
}

if (Math.HALF_PI === undefined) {
    Math.HALF_PI = Math.PI / 2;
}

// Thanks to Stackoverflow user fearphage
if (!String.format) {
    String.format = function (format) {
        var args = Array.prototype.slice.call(arguments, 1);
        return format.replace(/\{(\d+)\}/g, function (match, number) {
            return args[number] !== undefined ? args[number] : match;
        });
    };
}

var Clock = require('../common/clock'),
    settings = require('../common/settings'),
    controls = require('../client/controls'),
    simulator = require('../common/simulator').make(),
    commands = require('../common/commands'),
    level = require('../client/level'),
// Load rcon after level to avoid issues with cyclic deps
    rcon = require('../client/rcon'),
    scenemgr = require('../client/scene-manager'),
    protocol = require('../client/protocol'),
    arena = require('../common/arena'),
    client = require('../client/client'),
    console = require('../dom/console');

console.log("Playing Arena version {0}", arena.version);
if (arena.debug) {
    console.warn("Debug mode is enabled");
    window.debugging = true;
}

function update(time) {
    controls.update(time);
    simulator.update(time);
    scenemgr.copyWorldToScene();
}

settings.api.init();

protocol.clientInterface = {
    spawnFromDesc: level.spawnFromDesc
};

// Add command shorthand
console.executeFn = window.c = function (str) {
    return commands.execute(str, 'cl');
};

commands.register(level.commands);
commands.register(controls.commands);
commands.register(rcon.commands);
commands.register(client.commands);

protocol.simulator = simulator;

// Entry point
function entrypoint() {
    var display = require('../client/display');

    scenemgr.init(display.scene, simulator);

    controls.firstPersonCam(display.camera);
    controls.sceneObj.position.set(0, 2, 0);
    controls.physBody.position.set(0, 2, 0);
    controls.physBody.linearDamping = 0.95;
    display.scene.add(controls.sceneObj);
    simulator.add(controls.physBody, 0);

    commands.register(display.commands);

    display.render();

    Clock.startNew(16, update);
}

if (document.readyState === 'interactive') {
    entrypoint();
} else {
    document.addEventListener('DOMContentLoaded', entrypoint);
}<|endoftext|>


<commit_before>/**
 * @file 日志debug模块
 * @author xiaowu <fe.xiaowu@gmail.com>
 */

(function (window, $, Log) {
    'use strict';

    var debug = Log.debug = {};

    /**
     * 获取完成链接, 如果是以 / 开头, 则匹配当前的链接
     *
     * @param  {string} url 链接
     *
     * @return {string}
     */
    var getLogPath = function (url) {
        if (/^\/[^\/]/.test(url)) {
            url = location.origin + url;
        }
        return url;
    };

    /**
     * 日志链接最大长度
     *
     * @type {number}
     */
    debug.URL_MAX_LENGTH = 100;

    /**
     * 参数值最大长度
     *
     * @type {number}
     */
    debug.PARAM_MAX_LENGTH = 10;

    /**
     * 报错
     */
    debug.error = function () {
        /* istanbul ignore next */
        if (window.console && window.console.error) {
            console.error.apply(console, [].slice.call(arguments));
        }
    };

    /**
     * 警告
     */
    debug.warn = function () {
        /* istanbul ignore next */
        if (window.console && window.console.warn) {
            console.warn.apply(console, [].slice.call(arguments));
        }
    };

    /**
     * 拦截数据
     *
     * @param  {string}   name     名字
     * @param  {Function} callback 回调, 参数是调用时的参数, 只执行回调, 不阻止代码
     */
    debug.mock = function (name, callback) {
        var old;
        var type = name.split('.')[0].toLowerCase();
        var fn = function () {
            var context = this;
            var args = [].slice.call(arguments);

            callback.apply(context, args);

            return old.apply(context, args);
        };

        name = name.split('.')[1];

        if (type === 'log') {
            old = Log[name];
            Log[name] = fn;
        }
        else if (type === 'class') {
            old = Log.Class.prototype[name];
            Log.Class.prototype[name] = fn;
        }
        else {
            throw new TypeError('name错误');
        }

    };

    // 创建时校验参数
    debug.mock('Log.create', function (url, options) {
        if ('undefined' === typeof url) {
            debug.error('Log.create(url) - url不能为空');
        }
        else if ('string' !== typeof url || !url) {
            debug.error('Log.create(url) - url必须为非空字符串');
        }
        else if (options && !$.isPlainObject(options)) {
            debug.error('Log.create(url, options) - options必须为对象');
        }
    });

    // 检测全局覆盖时有没有参数重复
    // 检测空参数
    // 检测超长参数
    debug.mock('Class._makeGlobal', function (data) {
        var options = this.global;
        var params = {};

        // 循环全局参数, 判断send()参数里存在则报警
        $.each(options, function (key, value) {
            // global里支持fn
            if ($.isFunction(value)) {
                value = value();
            }

            if (data.hasOwnProperty(key)) {
                debug.warn([
                    'send里存在全局global里重复参数 "' + key + '" , ',
                    '参数值将由全局的 "' + value + '" 更新为 "' + data[key] + '"'
                ].join(''));
            }

            params[key] = value;
        });

        $.each(data, function (key, value) {
            params[key] = value;
        });

        $.each(params, function (key, value) {
            if ('undefined' === typeof value) {
                debug.warn('存在空参数: ' + key);
            }
            else if (String(value).length > debug.PARAM_MAX_LENGTH) {
                debug.warn('存在超长参数: ' + key + '=' + value + ', 长度为: ' + String(value).length);
            }
        });
    });

    // 在发送图片时检测参数
    debug.mock('Log._sendImg', function (data, url) {
        var length = Log._parseUrl(data, getLogPath(url)).length;

        if (length > debug.URL_MAX_LENGTH) {
            debug.warn('日志链接超长, 长度为: ' + length);
        }
    });
})(window, window.$, window.Log);
<commit_msg>添加空参数名<commit_after>/**
 * @file 日志debug模块
 * @author xiaowu <fe.xiaowu@gmail.com>
 */

(function (window, $, Log) {
    'use strict';

    var debug = Log.debug = {};

    /**
     * 获取完成链接, 如果是以 / 开头, 则匹配当前的链接
     *
     * @param  {string} url 链接
     *
     * @return {string}
     */
    var getLogPath = function (url) {
        if (/^\/[^\/]/.test(url)) {
            url = location.origin + url;
        }
        return url;
    };

    /**
     * 日志链接最大长度
     *
     * @type {number}
     */
    debug.URL_MAX_LENGTH = 100;

    /**
     * 参数值最大长度
     *
     * @type {number}
     */
    debug.PARAM_MAX_LENGTH = 10;

    /**
     * 报错
     */
    debug.error = function () {
        /* istanbul ignore next */
        if (window.console && window.console.error) {
            console.error.apply(console, [].slice.call(arguments));
        }
    };

    /**
     * 警告
     */
    debug.warn = function () {
        /* istanbul ignore next */
        if (window.console && window.console.warn) {
            console.warn.apply(console, [].slice.call(arguments));
        }
    };

    /**
     * 拦截数据
     *
     * @param  {string}   name     名字
     * @param  {Function} callback 回调, 参数是调用时的参数, 只执行回调, 不阻止代码
     */
    debug.mock = function (name, callback) {
        var old;
        var type = name.split('.')[0].toLowerCase();
        var fn = function () {
            var context = this;
            var args = [].slice.call(arguments);

            callback.apply(context, args);

            return old.apply(context, args);
        };

        name = name.split('.')[1];

        if (type === 'log') {
            old = Log[name];
            Log[name] = fn;
        }
        else if (type === 'class') {
            old = Log.Class.prototype[name];
            Log.Class.prototype[name] = fn;
        }
        else {
            throw new TypeError('name错误');
        }

    };

    // 创建时校验参数
    debug.mock('Log.create', function (url, options) {
        if ('undefined' === typeof url) {
            debug.error('Log.create(url) - url不能为空');
        }
        else if ('string' !== typeof url || !url) {
            debug.error('Log.create(url) - url必须为非空字符串');
        }
        else if (options && !$.isPlainObject(options)) {
            debug.error('Log.create(url, options) - options必须为对象');
        }
    });

    // 检测全局覆盖时有没有参数重复
    // 检测空参数
    // 检测超长参数
    debug.mock('Class._makeGlobal', function (data) {
        var options = this.global;
        var params = {};

        // 循环全局参数, 判断send()参数里存在则报警
        $.each(options, function (key, value) {
            // global里支持fn
            if ($.isFunction(value)) {
                value = value();
            }

            if (data.hasOwnProperty(key)) {
                debug.warn([
                    'send里存在全局global里重复参数 "' + key + '" , ',
                    '参数值将由全局的 "' + value + '" 更新为 "' + data[key] + '"'
                ].join(''));
            }

            params[key] = value;
        });

        $.each(data, function (key, value) {
            params[key] = value;
        });

        $.each(params, function (key, value) {
            if ('undefined' === typeof key || key === '') {
                debug.warn('存在空参数名');
            }
            else if ('undefined' === typeof value) {
                debug.warn('存在空参数: ' + key);
            }
            else if (String(value).length > debug.PARAM_MAX_LENGTH) {
                debug.warn('存在超长参数: ' + key + '=' + value + ', 长度为: ' + String(value).length);
            }
        });
    });

    // 在发送图片时检测参数
    debug.mock('Log._sendImg', function (data, url) {
        var length = Log._parseUrl(data, getLogPath(url)).length;

        if (length > debug.URL_MAX_LENGTH) {
            debug.warn('日志链接超长, 长度为: ' + length);
        }
    });
})(window, window.$, window.Log);
<|endoftext|>


<commit_before>function raw_data_transformation(args) {
  'use strict';

  // dupe our data so we can modify it without adverse effect
  args.data = MG.clone(args.data);

  // We need to account for a few data format cases:
  // #1 [{key:__, value:__}, ...]                // unnested obj-arrays
  // #2 [[{key:__, value:__}, ...], [{key:__, value:__}, ...]] // nested obj-arrays
  // #3 [[4323, 2343],..]                    // unnested 2d array
  // #4 [[[4323, 2343],..] , [[4323, 2343],..]]        // nested 2d array

  args.array_of_objects = false;
  args.array_of_arrays = false;
  args.nested_array_of_arrays = false;
  args.nested_array_of_objects = false;

  // is the data object a nested array?
  if (is_array_of_arrays(args.data)) {
    args.nested_array_of_objects = args.data.map(function(d) {
      return is_array_of_objects_or_empty(d);
    });                               // Case #2
    args.nested_array_of_arrays = args.data.map(function(d) {
      return is_array_of_arrays(d);
    });                               // Case #4
  } else {
    args.array_of_objects = is_array_of_objects(args.data);     // Case #1
    args.array_of_arrays = is_array_of_arrays(args.data);     // Case #3
  }

  if (args.chart_type === 'line') {
    if (args.array_of_objects || args.array_of_arrays) {
      args.data = [args.data];
    }
  } else {
    if (!(args.data[0] instanceof Array)) {
      args.data = [args.data];
    }
  }

  // if the y_accessor is an array, break it up and store the result in args.data
  mg_process_multiple_y_accessors(args);

  // if user supplies keyword in args.color, change to arg.colors.
  // this is so that the API remains fairly sensible and legible.
  if (args.color !== undefined) {
    args.colors = args.color;
  }

  // if user has supplied args.colors, and that value is a string, turn it into an array.
  if (args.colors !== null && typeof args.colors === 'string') {
    args.colors = [args.colors];
  }

  // sort x-axis data
  if (args.chart_type === 'line' && args.x_sort === true) {
    for (var i = 0; i < args.data.length; i++) {
      args.data[i].sort(function(a, b) {
        return a[args.x_accessor] - b[args.x_accessor];
      });
    }
  }

  return this;
}

function mg_process_multiple_y_accessors(args) {
  if (args.y_accessor instanceof Array) {
  args.data = args.data.map(function(_d) {
    return args.y_accessor.map(function(ya) {
    return _d.map(function(di) {
      di = MG.clone(di);

      if (di[ya] === undefined) {
      return undefined;
      }

      di['multiline_y_accessor'] = di[ya];
      return di;
    }).filter(function(di) {
      return di !== undefined;
    });
    });
  })[0];

  args.y_accessor = 'multiline_y_accessor';
  }
}

MG.raw_data_transformation = raw_data_transformation;

function process_line(args) {
  'use strict';

  var time_frame;

  //do we have a time-series?
  var is_time_series = d3.sum(args.data.map(function(series) {
    return series.length > 0 && series[0][args.x_accessor] instanceof Date;
  })) > 0;

  //force linear interpolation when missing_is_hidden is enabled
  if (args.missing_is_hidden) {
    args.interpolate = 'linear';
  }
  //are we replacing missing y values with zeros?
  if ((args.missing_is_zero || args.missing_is_hidden)
      && args.chart_type === 'line'
      && is_time_series
    ) {

    for (var i = 0; i < args.data.length; i++) {
      //we need to have a dataset of length > 2, so if it's less than that, skip
      if (args.data[i].length <= 1) {
        continue;
      }

      var first = args.data[i][0];
      var last = args.data[i][args.data[i].length-1];
      //initialize our new array for storing the processed data
      var processed_data = [];

      //we'll be starting from the day after our first date
      var start_date = MG.clone(first[args.x_accessor]).setDate(first[args.x_accessor].getDate() + 1);

      //if we've set a max_x, add data points up to there
      var from = (args.min_x) ? args.min_x : start_date;
      var upto = (args.max_x) ? args.max_x : last[args.x_accessor];

      time_frame = mg_get_time_frame((upto-from)/1000);

      if (time_frame == 'default' && args.missing_is_hidden_accessor === null) {
        for (var d = new Date(from); d <= upto; d.setDate(d.getDate() + 1)) {
          var o = {};
          d.setHours(0, 0, 0, 0);

          //add the first date item
          //we'll be starting from the day after our first date
          if (Date.parse(d) === Date.parse(new Date(start_date))) {
            processed_data.push(MG.clone(args.data[i][0]));
          }

          //check to see if we already have this date in our data object
          var existing_o = null;
          args.data[i].forEach(function(val, i) {
            if (Date.parse(val[args.x_accessor]) === Date.parse(new Date(d))) {
              existing_o = val;

              return false;
            }
          });

          //if we don't have this date in our data object, add it and set it to zero
          if (!existing_o) {
            o[args.x_accessor] = new Date(d);
            o[args.y_accessor] = 0;
            o['_missing'] = true; //we want to distinguish between zero-value and missing observations
            processed_data.push(o);
          }
          //if the data point has, say, a 'missing' attribute set or if its
          //y-value is null identify it internally as missing
          else if (existing_o[args.missing_is_hidden_accessor]
              || existing_o[args.y_accessor] === null
            ) {
            existing_o['_missing'] = true;
            processed_data.push(existing_o);
          }
          //otherwise, use the existing object for that date
          else {
            processed_data.push(existing_o);
          }
        }
      }
      else {
        for (var j = 0; j < args.data[i].length; j += 1) {
          var o = MG.clone(args.data[i][j]);
          o['_missing'] = args.data[i][j][args.missing_is_hidden_accessor];
          processed_data.push(o);
        }
      }

      //update our date object
      args.data[i] = processed_data;
    }
  }

  return this;
}

MG.process_line = process_line;

function process_histogram(args) {
  'use strict';

  // if args.binned=False, then we need to bin the data appropriately.
  // if args.binned=True, then we need to make sure to compute the relevant computed data.
  // the outcome of either of these should be something in args.computed_data.
  // the histogram plotting function will be looking there for the data to plot.

  // we need to compute an array of objects.
  // each object has an x, y, and dx.

  // histogram data is always single dimension
  var our_data = args.data[0];

  var extracted_data;
  if (args.binned === false) {
    // use d3's built-in layout.histogram functionality to compute what you need.

    if (typeof(our_data[0]) === 'object') {
      // we are dealing with an array of objects. Extract the data value of interest.
      extracted_data = our_data
        .map(function(d) {
          return d[args.x_accessor];
        });
    } else if (typeof(our_data[0]) === 'number') {
      // we are dealing with a simple array of numbers. No extraction needed.
      extracted_data = our_data;
    } else {
      console.log('TypeError: expected an array of numbers, found ' + typeof(our_data[0]));
      return;
    }

    var hist = d3.layout.histogram();
    if (args.bins) {
      hist = hist.bins(args.bins);
    }

    args.processed_data = hist(extracted_data)
      .map(function(d) {
        // extract only the data we need per data point.
        return {'x': d.x, 'y': d.y, 'dx': d.dx};
      });
  } else {
    // here, we just need to reconstruct the array of objects
    // take the x accessor and y accessor.
    // pull the data as x and y. y is count.

    args.processed_data = our_data.map(function(d) {
      return {'x': d[args.x_accessor], 'y': d[args.y_accessor]};
    });

    var this_pt;
    var next_pt;

    // we still need to compute the dx component for each data point
    for (var i=0; i < args.processed_data.length; i++) {
      this_pt = args.processed_data[i];
      if (i === args.processed_data.length - 1) {
        this_pt.dx = args.processed_data[i-1].dx;
      } else {
        next_pt = args.processed_data[i+1];
        this_pt.dx = next_pt.x - this_pt.x;
      }
    }
  }

  // capture the original data and accessors before replacing args.data
  if (!args.processed) {
    args.processed = {};
  }
  args.processed.original_data = args.data;
  args.processed.original_x_accessor = args.x_accessor;
  args.processed.original_y_accessor = args.y_accessor;

  args.data = [args.processed_data];
  args.x_accessor = args.processed_x_accessor;
  args.y_accessor = args.processed_y_accessor;

  return this;
}

MG.process_histogram = process_histogram;

// for use with bar charts, etc.
function process_categorical_variables(args) {
  'use strict';

  var extracted_data, processed_data={}, pd=[];
  var our_data = args.data[0];
  var label_accessor = args.bar_orientation === 'vertical' ? args.x_accessor : args.y_accessor;
  var data_accessor =  args.bar_orientation === 'vertical' ? args.y_accessor : args.x_accessor;

  args.categorical_variables = [];
  if (args.binned === false) {
    if (typeof(our_data[0]) === 'object') {
      // we are dealing with an array of objects. Extract the data value of interest.
      extracted_data = our_data
        .map(function(d) {
          return d[label_accessor];
        });
    } else {
      extracted_data = our_data;
    }

    var this_dp;

    for (var i = 0; i < extracted_data.length; i++) {
      this_dp=extracted_data[i];
      if (args.categorical_variables.indexOf(this_dp) === -1) args.categorical_variables.push(this_dp);
      if (!processed_data.hasOwnProperty(this_dp)) processed_data[this_dp] = 0;

      processed_data[this_dp] += 1;
    }

    processed_data = Object.keys(processed_data).map(function(d) {
      var obj = {};
      obj[data_accessor] = processed_data[d];
      obj[label_accessor] = d;
      return obj;
    });
  } else {
    // nothing needs to really happen here.
    processed_data = our_data;
    args.categorical_variables = d3.set(processed_data.map(function(d) {
      return d[label_accessor];
    })).values();
    args.categorical_variables.reverse();
  }

  args.data = [processed_data];
  return this;
}

MG.process_categorical_variables = process_categorical_variables;

function process_point(args) {
  'use strict';

  var data = args.data[0];
  var x = data.map(function(d) { return d[args.x_accessor]; });
  var y = data.map(function(d) { return d[args.y_accessor]; });

  if (args.least_squares) {
    args.ls_line = least_squares(x,y);
  }

  //args.lowess_line = lowess_robust(x,y, .5, 100)
  return this;
}

MG.process_point = process_point;
<commit_msg>Fix #556<commit_after>function raw_data_transformation(args) {
  'use strict';

  // dupe our data so we can modify it without adverse effect
  args.data = MG.clone(args.data);

  // we need to account for a few data format cases:
  // #1 [{key:__, value:__}, ...]                               // unnested obj-arrays
  // #2 [[{key:__, value:__}, ...], [{key:__, value:__}, ...]]  // nested obj-arrays
  // #3 [[4323, 2343],..]                                       // unnested 2d array
  // #4 [[[4323, 2343],..] , [[4323, 2343],..]]                 // nested 2d array

  args.array_of_objects = false;
  args.array_of_arrays = false;
  args.nested_array_of_arrays = false;
  args.nested_array_of_objects = false;

  // is the data object a nested array?
  if (is_array_of_arrays(args.data)) {
    args.nested_array_of_objects = args.data.map(function(d) {
      return is_array_of_objects_or_empty(d);
    });                               // Case #2
    args.nested_array_of_arrays = args.data.map(function(d) {
      return is_array_of_arrays(d);
    });                               // Case #4
  } else {
    args.array_of_objects = is_array_of_objects(args.data);     // Case #1
    args.array_of_arrays = is_array_of_arrays(args.data);     // Case #3
  }

  if (args.chart_type === 'line') {
    if (args.array_of_objects || args.array_of_arrays) {
      args.data = [args.data];
    }
  } else {
    if (!(args.data[0] instanceof Array)) {
      args.data = [args.data];
    }
  }

  // if the y_accessor is an array, break it up and store the result in args.data
  mg_process_multiple_y_accessors(args);

  // if user supplies keyword in args.color, change to arg.colors.
  // this is so that the API remains fairly sensible and legible.
  if (args.color !== undefined) {
    args.colors = args.color;
  }

  // if user has supplied args.colors, and that value is a string, turn it into an array.
  if (args.colors !== null && typeof args.colors === 'string') {
    args.colors = [args.colors];
  }

  // sort x-axis data
  if (args.chart_type === 'line' && args.x_sort === true) {
    for (var i = 0; i < args.data.length; i++) {
      args.data[i].sort(function(a, b) {
        return a[args.x_accessor] - b[args.x_accessor];
      });
    }
  }

  return this;
}

function mg_process_multiple_y_accessors(args) {
  if (args.y_accessor instanceof Array) {
    args.data = args.data.map(function(_d) {
      return args.y_accessor.map(function(ya) {
        return _d.map(function(di) {
          di = MG.clone(di);

          if (di[ya] === undefined) {
            return undefined;
          }

          di['multiline_y_accessor'] = di[ya];
          return di;
        }).filter(function(di) {
          return di !== undefined;
        });
      });
    })[0];

    args.y_accessor = 'multiline_y_accessor';
  }
}

MG.raw_data_transformation = raw_data_transformation;

function process_line(args) {
  'use strict';

  var time_frame;

  // do we have a time-series?
  var is_time_series = d3.sum(args.data.map(function(series) {
    return series.length > 0 && series[0][args.x_accessor] instanceof Date;
  })) > 0;

  // force linear interpolation when missing_is_hidden is enabled
  if (args.missing_is_hidden) {
    args.interpolate = 'linear';
  }

  // are we replacing missing y values with zeros?
  if ((args.missing_is_zero || args.missing_is_hidden)
      && args.chart_type === 'line'
      && is_time_series
    ) {
    for (var i = 0; i < args.data.length; i++) {
      // we need to have a dataset of length > 2, so if it's less than that, skip
      if (args.data[i].length <= 1) {
        continue;
      }

      var first = args.data[i][0];
      var last = args.data[i][args.data[i].length-1];

      // initialize our new array for storing the processed data
      var processed_data = [];

      // we'll be starting from the day after our first date
      var start_date = MG.clone(first[args.x_accessor]).setDate(first[args.x_accessor].getDate() + 1);

      // if we've set a max_x, add data points up to there
      var from = (args.min_x) ? args.min_x : start_date;
      var upto = (args.max_x) ? args.max_x : last[args.x_accessor];

      time_frame = mg_get_time_frame((upto-from)/1000);

      if (time_frame == 'default' && args.missing_is_hidden_accessor === null) {
        for (var d = new Date(from); d <= upto; d.setDate(d.getDate() + 1)) {
          var o = {};
          d.setHours(0, 0, 0, 0);

          // add the first date item, we'll be starting from the day after our first date
          if (Date.parse(d) === Date.parse(new Date(start_date))) {
            processed_data.push(MG.clone(args.data[i][0]));
          }

          // check to see if we already have this date in our data object
          var existing_o = null;
          args.data[i].forEach(function(val, i) {
            if (Date.parse(val[args.x_accessor]) === Date.parse(new Date(d))) {
              existing_o = val;

              return false;
            }
          });

          // if we don't have this date in our data object, add it and set it to zero
          if (!existing_o) {
            o[args.x_accessor] = new Date(d);
            o[args.y_accessor] = 0;
            o['_missing'] = true; //we want to distinguish between zero-value and missing observations
            processed_data.push(o);
          }

          // if the data point has, say, a 'missing' attribute set or if its
          // y-value is null identify it internally as missing
          else if (existing_o[args.missing_is_hidden_accessor]
              || existing_o[args.y_accessor] === null
            ) {
            existing_o['_missing'] = true;
            processed_data.push(existing_o);
          }

          //otherwise, use the existing object for that date
          else {
            processed_data.push(existing_o);
          }
        }
      } else {
        for (var j = 0; j < args.data[i].length; j += 1) {
          var o = MG.clone(args.data[i][j]);
          o['_missing'] = args.data[i][j][args.missing_is_hidden_accessor];
          processed_data.push(o);
        }
      }

      // update our date object
      args.data[i] = processed_data;
    }
  }

  return this;
}

MG.process_line = process_line;

function process_histogram(args) {
  'use strict';

  // if args.binned == false, then we need to bin the data appropriately.
  // if args.binned == true, then we need to make sure to compute the relevant computed data.
  // the outcome of either of these should be something in args.computed_data.
  // the histogram plotting function will be looking there for the data to plot.

  // we need to compute an array of objects.
  // each object has an x, y, and dx.

  // histogram data is always single dimension
  var our_data = args.data[0];

  var extracted_data;
  if (args.binned === false) {
    // use d3's built-in layout.histogram functionality to compute what you need.

    if (typeof(our_data[0]) === 'object') {
      // we are dealing with an array of objects. Extract the data value of interest.
      extracted_data = our_data
        .map(function(d) {
          return d[args.x_accessor];
        });
    } else if (typeof(our_data[0]) === 'number') {
      // we are dealing with a simple array of numbers. No extraction needed.
      extracted_data = our_data;
    } else {
      console.log('TypeError: expected an array of numbers, found ' + typeof(our_data[0]));
      return;
    }

    var hist = d3.layout.histogram();
    if (args.bins) {
      hist = hist.bins(args.bins);
    }

    args.processed_data = hist(extracted_data)
      .map(function(d) {
        // extract only the data we need per data point.
        return {'x': d.x, 'y': d.y, 'dx': d.dx};
      });
  } else {
    // here, we just need to reconstruct the array of objects
    // take the x accessor and y accessor.
    // pull the data as x and y. y is count.

    args.processed_data = our_data.map(function(d) {
      return {'x': d[args.x_accessor], 'y': d[args.y_accessor]};
    });

    var this_pt;
    var next_pt;

    // we still need to compute the dx component for each data point
    for (var i=0; i < args.processed_data.length; i++) {
      this_pt = args.processed_data[i];
      if (i === args.processed_data.length - 1) {
        this_pt.dx = args.processed_data[i-1].dx;
      } else {
        next_pt = args.processed_data[i+1];
        this_pt.dx = next_pt.x - this_pt.x;
      }
    }
  }

  // capture the original data and accessors before replacing args.data
  if (!args.processed) {
    args.processed = {};
  }
  args.processed.original_data = args.data;
  args.processed.original_x_accessor = args.x_accessor;
  args.processed.original_y_accessor = args.y_accessor;

  args.data = [args.processed_data];
  args.x_accessor = args.processed_x_accessor;
  args.y_accessor = args.processed_y_accessor;

  return this;
}

MG.process_histogram = process_histogram;

// for use with bar charts, etc.
function process_categorical_variables(args) {
  'use strict';

  var extracted_data, processed_data={}, pd=[];
  var our_data = args.data[0];
  var label_accessor = args.bar_orientation === 'vertical' ? args.x_accessor : args.y_accessor;
  var data_accessor =  args.bar_orientation === 'vertical' ? args.y_accessor : args.x_accessor;

  args.categorical_variables = [];
  if (args.binned === false) {
    if (typeof(our_data[0]) === 'object') {
      // we are dealing with an array of objects, extract the data value of interest
      extracted_data = our_data
        .map(function(d) {
          return d[label_accessor];
        });
    } else {
      extracted_data = our_data;
    }

    var this_dp;

    for (var i = 0; i < extracted_data.length; i++) {
      this_dp=extracted_data[i];
      if (args.categorical_variables.indexOf(this_dp) === -1) args.categorical_variables.push(this_dp);
      if (!processed_data.hasOwnProperty(this_dp)) processed_data[this_dp] = 0;

      processed_data[this_dp] += 1;
    }

    processed_data = Object.keys(processed_data).map(function(d) {
      var obj = {};
      obj[data_accessor] = processed_data[d];
      obj[label_accessor] = d;
      return obj;
    });
  } else {
    // nothing needs to really happen here
    processed_data = our_data;
    args.categorical_variables = d3.set(processed_data.map(function(d) {
      return d[label_accessor];
    })).values();
    args.categorical_variables.reverse();
  }

  args.data = [processed_data];
  return this;
}

MG.process_categorical_variables = process_categorical_variables;

function process_point(args) {
  'use strict';

  var data = args.data[0];
  var x = data.map(function(d) { return d[args.x_accessor]; });
  var y = data.map(function(d) { return d[args.y_accessor]; });

  if (args.least_squares) {
    args.ls_line = least_squares(x,y);
  }

  return this;
}

MG.process_point = process_point;
<|endoftext|>


<commit_before>define('onion-editor',[
  'scribe',
  'scribe-plugin-blockquote-command',
  'scribe-plugin-curly-quotes',
  'scribe-plugin-formatter-plain-text-convert-new-lines-to-html',
  'scribe-plugin-heading-command',
  'scribe-plugin-intelligent-unlink-command',
  'scribe-plugin-keyboard-shortcuts',
  'scribe-plugin-link-ui',
  'scribe-plugin-sanitizer',
  'scribe-plugin-smart-lists',
  'scribe-plugin-toolbar',
  'scribe-plugin-inline-objects',
  'scribe-plugin-betty-cropper',
  'scribe-plugin-youtube',
  'scribe-plugin-embed',
  'scribe-plugin-embed-instagram',
  'scribe-plugin-onion-video',
  'scribe-plugin-hr',
  'scribe-plugin-placeholder',
  'link-formatter',
  'only-trailing-brs',
  'paste-strip-newlines',
  'paste-strip-nbsps',
  'paste-from-word',
  'paste-sanitize',
  'remove-a-styles',
  'strip-bold-in-headings',
  'scribe-plugin-anchor',
  // scribe core
  'our-ensure-selectable-containers',
  'enforce-p-elements',
  'filter-for-export',
], function (
  Scribe,
  scribePluginBlockquoteCommand,
  scribePluginCurlyQuotes,
  scribePluginFormatterPlainTextConvertNewLinesToHtml,
  scribePluginHeadingCommand,
  scribePluginIntelligentUnlinkCommand,
  scribePluginKeyboardShortcuts,
  scribePluginLinkUI,
  scribePluginSanitizer,
  scribePluginSmartLists,
  scribePluginToolbar,
  scribePluginInlineObjects,
  scribePluginBettyCropper,
  scribePluginYoutube,
  scribePluginEmbed,
  scribePluginEmbedInstagram,
  scribePluginOnionVideo,
  scribePluginHr,
  scribePluginPlaceholder,
  linkFormatter,
  onlyTrailingBrs,
  pasteStripNewlines,
  pasteStripNbsps,
  pasteFromWord,
  pasteSanitize,
  removeAStyles,
  stripBoldInHeadings,
  scribePluginAnchor,
  // scribe core
  ourEnsureSelectableContainers,
  enforcePElements,
  filterForExport
) {

  'use strict';

  var defaults = {
    multiline: true,
    formatting: ['link', 'bold', 'italic', 'blockquote', 'heading', 'list', 'underline'],
    link: {
      domain: 'avclub.com'
    },
    video: {
      videoEmbedUrl: 'http://example.com?videoid=',
      insertDialog: function() {  },
      editDialog: function() {  }
    },
    image: {
      insertDialog: function() {  },
      editDialog: function() {  }
    }
  };

  function OnionEditor(element, options) {
    options = $.extend(defaults, options);
    $('.inline', element).attr('contenteditable', 'false');

    var scribe = new Scribe(element, { allowBlockElements: options.multiline });

    /* if a node running through the sanitizer passes this test, it won't get sanitized true */
    function skipSanitization(node) {
      return ($(node).is('div.inline'));
    };
    // HACK: we reset the default htmlFormatters 'normalize' because
    // they don't quite work with what we're doing and there's
    // apparently no other way to override/remove the offending ones.
    scribe._htmlFormatterFactory.formatters['normalize'] = [];
    if (scribe.allowsBlockElements()) {
      scribe.use(enforcePElements());
      scribe.use(ourEnsureSelectableContainers({skipElement: skipSanitization}));
    }

    // MO' HACKZ: We don't want to kill SPANS inside our inline divs
    var insertHTMLCommandPatch = new scribe.api.CommandPatch('insertHTML');
    insertHTMLCommandPatch.execute = function (value) {
      scribe.transactionManager.run(function () {
        scribe.api.CommandPatch.prototype.execute.call(this, value);

        sanitize(scribe.el);

        function sanitize(parentNode) {
          var treeWalker = document.createTreeWalker(parentNode, NodeFilter.SHOW_ELEMENT);
          var node = treeWalker.firstChild();
          if (!node) { return; }

          do {
            if (node.nodeName === 'SPAN' && node.className.indexOf('inline') === -1) {
              element.unwrap(parentNode, node);
            } else {
              /**
               * If the list item contains inline elements such as
               * A, B, or I, Chrome will also append an inline style for
               * `line-height` on those elements, so we remove it here.
               */
              node.style.lineHeight = null;

              // There probably wasn’t a `style` attribute before, so
              // remove it if it is now empty.
              if (node.getAttribute('style') === '') {
                node.removeAttribute('style');
              }
            }

            // Sanitize children
            sanitize(node);
          } while ((node = treeWalker.nextSibling()));
        }
      }.bind(this));
    };
    scribe.commandPatches.insertHTML = insertHTMLCommandPatch;

    // ENDHACK

    if (options.placeholder) {
      scribe.use(scribePluginPlaceholder(options.placeholder));
    }

    // For now, we need to patch some scribe commands, just in case.
    var boldCommand = new scribe.api.CommandPatch('bold');
    boldCommand.execute = function (value) {
      if (this.selection === undefined) {
        document.execCommand(this.commandName, false, value || null);
      } else {
        scribe.transactionManager.run(function () {
          document.execCommand(this.commandName, false, value || null);
        }.bind(this));
      }
    };
    scribe.commandPatches['bold'] = boldCommand;

    var italicCommand = new scribe.api.CommandPatch('italic');
    italicCommand.execute = function (value) {
      if (this.selection === undefined) {
        document.execCommand(this.commandName, false, value || null);
      } else {
        scribe.transactionManager.run(function () {
          document.execCommand(this.commandName, false, value || null);
        }.bind(this));
      }
    };
    scribe.commandPatches['italic'] = italicCommand;

    var underlineCommand = new scribe.api.CommandPatch('underline');
    underlineCommand.execute = function (value) {
      if (this.selection === undefined) {
        document.execCommand(this.commandName, false, value || null);
      } else {
        scribe.transactionManager.run(function () {
          document.execCommand(this.commandName, false, value || null);
        }.bind(this));
      }
    };
    scribe.commandPatches['underline'] = underlineCommand;
    // End horrible patches

    var keyCommands = {};
    var ctrlKey = function (event) { return event.metaKey || event.ctrlKey; };

    // Allowable Tags
    var tags = {};

    // Multiline
    if (options.multiline) {
      tags.p = {'id': true};
      tags.br = {};
      tags.hr = {};
    } else {
      // tags.br = {};
      // scribe.use(onlyTrailingBrs());
    }

    // Bold
    if (options.formatting.indexOf('bold') !== -1) {
      keyCommands.bold = function (event) { return event.metaKey && event.keyCode === 66; }; // b
      tags.b = {'id': true};
    }

    // Italics
    if (options.formatting.indexOf('italic') !== -1) {
      keyCommands.italic = function (event) { return event.metaKey && event.keyCode === 73; }; // i
      tags.i = {'id': true};
      tags.em = {'id': true};
    }

    // Strike
    if (options.formatting.indexOf('strike') !== -1) {
      keyCommands.strikeThrough = function (event) { return event.altKey && event.shiftKey && event.keyCode === 83; }; // strike
      tags.strike = {'id': true};
    }

    // Underline
    if (options.formatting.indexOf('underline') !== -1) {
      keyCommands.underline = function (event) { return event.metaKey && event.keyCode === 85; }; // u
      tags.u = {'id': true};
    }

    // Remove formatting...
    keyCommands.removeFormat = function (event) { return event.altKey && event.shiftKey && event.keyCode === 65; }; // a

    // Links
    if (options.multiline && options.formatting.indexOf('link') !== -1) {
      keyCommands.linkUI = function (event) { return event.metaKey && ! event.shiftKey && event.keyCode === 75; }; // k
      keyCommands.unlink = function (event) { return event.metaKey && event.shiftKey && event.keyCode === 75; }; // k,
      scribe.use(scribePluginIntelligentUnlinkCommand());
      scribe.use(scribePluginLinkUI(options.link));
      scribe.use(linkFormatter(options.link));
      tags.a = { href:true, target:true, id:true};
    }

    // Lists
    if (options.multiline && options.formatting.indexOf('list') !== -1) {
      keyCommands.insertUnorderedList = function (event) { return event.altKey && event.shiftKey && event.keyCode === 66; }; // b
      keyCommands.insertOrderedList = function (event) { return event.altKey && event.shiftKey && event.keyCode === 78; }; // n

      scribe.use(scribePluginSmartLists());
      tags.ol = {id:true};
      tags.ul = {id:true};
      tags.li = {id:true};
    }

    //Blockquotes
    if (options.multiline && options.formatting.indexOf('blockquote') !== -1) {
      keyCommands.blockquote = function (event) { return event.altKey && event.shiftKey && event.keyCode === 87; }; // w
      scribe.use(scribePluginBlockquoteCommand());
      tags.blockquote = {id:true};
    }

    // Headings
    if (options.multiline && options.formatting.indexOf('heading') !== -1) {
      keyCommands.h3 = function (event) { return ctrlKey(event) && event.keyCode === 50; }; // 2
      keyCommands.h4 = function (event) { return ctrlKey(event) && event.keyCode === 51; }; // 2
      scribe.use(scribePluginHeadingCommand(3));
      scribe.use(scribePluginHeadingCommand(4));
      tags.h3 = {id:true};
      tags.h4 = {id:true};
    }

    // Inline Objects
    if (options.multiline && options.inlineObjects) {
      scribe.use(scribePluginInlineObjects(options.inlineObjects));

      // Maybe make optionally load these similar to formatting. For now, it's an all or nothing.

      scribe.use(scribePluginBettyCropper(options.image));
      scribe.use(scribePluginYoutube());
      scribe.use(scribePluginEmbed());
      scribe.use(scribePluginEmbedInstagram());
      scribe.use(scribePluginHr());
      scribe.use(scribePluginAnchor());
      scribe.use(scribePluginOnionVideo(options.video));
      scribe.use(removeAStyles());
      scribe.use(stripBoldInHeadings());
    }

    scribe.use(scribePluginSanitizer({
      tags: tags,
      skipSanitization: skipSanitization
    }));
    scribe.use(pasteFromWord());
    scribe.use(pasteSanitize());
    scribe.use(pasteStripNewlines());
    scribe.use(pasteStripNbsps());
    // Word count

    if (options.statsContainer) {
      setInterval(function () {
        $(options.statsContainer).html(
          $(scribe.el).text().split(' ').length
        );
      }, 3000);
    }

    // Per-Element Export Filtering
    scribe.use(filterForExport());

    /* This is necessary for a few dumb reasons. Scribe's transaction manager doesn't work when there
      ins't a selection inside of the editor. This means any changes made when the editor ins't in focus,
      like adding an image, stuff breaks. This works around that particular issue.

      I'm not really sure the right way to fix this or how to avoid this problem.

      The scroll stuff is a consequence of this.
    */
    scribe.updateContents = function(fn, skipFormatters) {
      // Default is to skipFormatters. Only place this needs to be set to false is when updating links.
      // We want formatters to run on links. Embeds & other shit seem to get sanitized
      // despite there being safegaurds for that.
      if (typeof skipFormatters === 'undefined') {
        skipFormatters = true;
      }
      scribe._skipFormatters = skipFormatters;
      var scrollY = window.scrollY;
      setTimeout(function() {
        scribe.el.focus();
        setTimeout(function() {
          scribe.transactionManager.run(fn);
          window.scrollTo(0, scrollY);

          // This should notify any changes that happen outside of typing
          scribe.trigger('content-changed');
        }, 20);
      }, 20);
    };

    scribe.use(scribePluginCurlyQuotes());
    scribe.use(scribePluginKeyboardShortcuts(Object.freeze(keyCommands)));

    // garbage toolbar setup
    if (options.multiline || options.singleLineUseToolbar) {
      var toolbarSelector = '.document-tools .toolbar-contents';
      var $toolbar = $(scribe.el.parentNode).find(toolbarSelector);

      scribe.use(scribePluginToolbar($toolbar[0], options));

      $toolbar.hide();
      $(scribe.el)
        .on('focus', function () {
          $toolbar.show();
          $(toolbarSelector).not($toolbar).hide();
        });
    } else {
      $(element.parentNode).find('.document-tools .toolbar-contents').hide();
    }

    // a little hacky to prevent deletion of images and other inline elements via the backspace key.
    scribe.el.addEventListener('keydown', function(event) {
      if (event.keyCode === 8) {
        // is the previous immediate child of editor an inline item?
        var sel = new scribe.api.Selection();
        var prev = $(sel.selection.anchorNode).closest('.editor>*').prev();
        if (prev.hasClass('inline')
          && sel.selection.anchorOffset === 0
          && sel.selection.isCollapsed) {
          event.preventDefault();
        }
      }
    });

    scribe.use(scribePluginFormatterPlainTextConvertNewLinesToHtml());

    this.setChangeHandler = function(func) {
      scribe.on('content-changed', func);
    };

    this.setContent = function(content) {
      if (!content) {
          content = '

';
      }
      scribe.setContent(content);
    };

    this.getContent = function() {
      //todo: if multiline is false, only return contents of the paragraph

      var contents = scribe.getContent();

      // Allow any plugins to clean up markup. Main use case is for embed plugin, atm.
      return contents;
    };

    this.setupToolbar = setupToolbar;

    this.scribe = scribe;
    return this;
  }

  return OnionEditor;

});
<commit_msg>remove single line thing<commit_after>define('onion-editor',[
  'scribe',
  'scribe-plugin-blockquote-command',
  'scribe-plugin-curly-quotes',
  'scribe-plugin-formatter-plain-text-convert-new-lines-to-html',
  'scribe-plugin-heading-command',
  'scribe-plugin-intelligent-unlink-command',
  'scribe-plugin-keyboard-shortcuts',
  'scribe-plugin-link-ui',
  'scribe-plugin-sanitizer',
  'scribe-plugin-smart-lists',
  'scribe-plugin-toolbar',
  'scribe-plugin-inline-objects',
  'scribe-plugin-betty-cropper',
  'scribe-plugin-youtube',
  'scribe-plugin-embed',
  'scribe-plugin-embed-instagram',
  'scribe-plugin-onion-video',
  'scribe-plugin-hr',
  'scribe-plugin-placeholder',
  'link-formatter',
  'only-trailing-brs',
  'paste-strip-newlines',
  'paste-strip-nbsps',
  'paste-from-word',
  'paste-sanitize',
  'remove-a-styles',
  'strip-bold-in-headings',
  'scribe-plugin-anchor',
  // scribe core
  'our-ensure-selectable-containers',
  'enforce-p-elements',
  'filter-for-export',
], function (
  Scribe,
  scribePluginBlockquoteCommand,
  scribePluginCurlyQuotes,
  scribePluginFormatterPlainTextConvertNewLinesToHtml,
  scribePluginHeadingCommand,
  scribePluginIntelligentUnlinkCommand,
  scribePluginKeyboardShortcuts,
  scribePluginLinkUI,
  scribePluginSanitizer,
  scribePluginSmartLists,
  scribePluginToolbar,
  scribePluginInlineObjects,
  scribePluginBettyCropper,
  scribePluginYoutube,
  scribePluginEmbed,
  scribePluginEmbedInstagram,
  scribePluginOnionVideo,
  scribePluginHr,
  scribePluginPlaceholder,
  linkFormatter,
  onlyTrailingBrs,
  pasteStripNewlines,
  pasteStripNbsps,
  pasteFromWord,
  pasteSanitize,
  removeAStyles,
  stripBoldInHeadings,
  scribePluginAnchor,
  // scribe core
  ourEnsureSelectableContainers,
  enforcePElements,
  filterForExport
) {

  'use strict';

  var defaults = {
    multiline: true,
    formatting: ['link', 'bold', 'italic', 'blockquote', 'heading', 'list', 'underline'],
    link: {
      domain: 'avclub.com'
    },
    video: {
      videoEmbedUrl: 'http://example.com?videoid=',
      insertDialog: function() {  },
      editDialog: function() {  }
    },
    image: {
      insertDialog: function() {  },
      editDialog: function() {  }
    }
  };

  function OnionEditor(element, options) {
    options = $.extend(defaults, options);
    $('.inline', element).attr('contenteditable', 'false');

    var scribe = new Scribe(element, { allowBlockElements: options.multiline });

    /* if a node running through the sanitizer passes this test, it won't get sanitized true */
    function skipSanitization(node) {
      return ($(node).is('div.inline'));
    };
    // HACK: we reset the default htmlFormatters 'normalize' because
    // they don't quite work with what we're doing and there's
    // apparently no other way to override/remove the offending ones.
    scribe._htmlFormatterFactory.formatters['normalize'] = [];
    if (scribe.allowsBlockElements()) {
      scribe.use(enforcePElements());
      scribe.use(ourEnsureSelectableContainers({skipElement: skipSanitization}));
    }

    // MO' HACKZ: We don't want to kill SPANS inside our inline divs
    var insertHTMLCommandPatch = new scribe.api.CommandPatch('insertHTML');
    insertHTMLCommandPatch.execute = function (value) {
      scribe.transactionManager.run(function () {
        scribe.api.CommandPatch.prototype.execute.call(this, value);

        sanitize(scribe.el);

        function sanitize(parentNode) {
          var treeWalker = document.createTreeWalker(parentNode, NodeFilter.SHOW_ELEMENT);
          var node = treeWalker.firstChild();
          if (!node) { return; }

          do {
            if (node.nodeName === 'SPAN' && node.className.indexOf('inline') === -1) {
              element.unwrap(parentNode, node);
            } else {
              /**
               * If the list item contains inline elements such as
               * A, B, or I, Chrome will also append an inline style for
               * `line-height` on those elements, so we remove it here.
               */
              node.style.lineHeight = null;

              // There probably wasn’t a `style` attribute before, so
              // remove it if it is now empty.
              if (node.getAttribute('style') === '') {
                node.removeAttribute('style');
              }
            }

            // Sanitize children
            sanitize(node);
          } while ((node = treeWalker.nextSibling()));
        }
      }.bind(this));
    };
    scribe.commandPatches.insertHTML = insertHTMLCommandPatch;

    // ENDHACK

    if (options.placeholder) {
      scribe.use(scribePluginPlaceholder(options.placeholder));
    }

    // For now, we need to patch some scribe commands, just in case.
    var boldCommand = new scribe.api.CommandPatch('bold');
    boldCommand.execute = function (value) {
      if (this.selection === undefined) {
        document.execCommand(this.commandName, false, value || null);
      } else {
        scribe.transactionManager.run(function () {
          document.execCommand(this.commandName, false, value || null);
        }.bind(this));
      }
    };
    scribe.commandPatches['bold'] = boldCommand;

    var italicCommand = new scribe.api.CommandPatch('italic');
    italicCommand.execute = function (value) {
      if (this.selection === undefined) {
        document.execCommand(this.commandName, false, value || null);
      } else {
        scribe.transactionManager.run(function () {
          document.execCommand(this.commandName, false, value || null);
        }.bind(this));
      }
    };
    scribe.commandPatches['italic'] = italicCommand;

    var underlineCommand = new scribe.api.CommandPatch('underline');
    underlineCommand.execute = function (value) {
      if (this.selection === undefined) {
        document.execCommand(this.commandName, false, value || null);
      } else {
        scribe.transactionManager.run(function () {
          document.execCommand(this.commandName, false, value || null);
        }.bind(this));
      }
    };
    scribe.commandPatches['underline'] = underlineCommand;
    // End horrible patches

    var keyCommands = {};
    var ctrlKey = function (event) { return event.metaKey || event.ctrlKey; };

    // Allowable Tags
    var tags = {};

    // Multiline
    if (options.multiline) {
      tags.p = {'id': true};
      tags.br = {};
      tags.hr = {};
    } else {
      // tags.br = {};
      // scribe.use(onlyTrailingBrs());
    }

    // Bold
    if (options.formatting.indexOf('bold') !== -1) {
      keyCommands.bold = function (event) { return event.metaKey && event.keyCode === 66; }; // b
      tags.b = {'id': true};
    }

    // Italics
    if (options.formatting.indexOf('italic') !== -1) {
      keyCommands.italic = function (event) { return event.metaKey && event.keyCode === 73; }; // i
      tags.i = {'id': true};
      tags.em = {'id': true};
    }

    // Strike
    if (options.formatting.indexOf('strike') !== -1) {
      keyCommands.strikeThrough = function (event) { return event.altKey && event.shiftKey && event.keyCode === 83; }; // strike
      tags.strike = {'id': true};
    }

    // Underline
    if (options.formatting.indexOf('underline') !== -1) {
      keyCommands.underline = function (event) { return event.metaKey && event.keyCode === 85; }; // u
      tags.u = {'id': true};
    }

    // Remove formatting...
    keyCommands.removeFormat = function (event) { return event.altKey && event.shiftKey && event.keyCode === 65; }; // a

    // Links
    if (options.multiline && options.formatting.indexOf('link') !== -1) {
      keyCommands.linkUI = function (event) { return event.metaKey && ! event.shiftKey && event.keyCode === 75; }; // k
      keyCommands.unlink = function (event) { return event.metaKey && event.shiftKey && event.keyCode === 75; }; // k,
      scribe.use(scribePluginIntelligentUnlinkCommand());
      scribe.use(scribePluginLinkUI(options.link));
      scribe.use(linkFormatter(options.link));
      tags.a = { href:true, target:true, id:true};
    }

    // Lists
    if (options.multiline && options.formatting.indexOf('list') !== -1) {
      keyCommands.insertUnorderedList = function (event) { return event.altKey && event.shiftKey && event.keyCode === 66; }; // b
      keyCommands.insertOrderedList = function (event) { return event.altKey && event.shiftKey && event.keyCode === 78; }; // n

      scribe.use(scribePluginSmartLists());
      tags.ol = {id:true};
      tags.ul = {id:true};
      tags.li = {id:true};
    }

    //Blockquotes
    if (options.multiline && options.formatting.indexOf('blockquote') !== -1) {
      keyCommands.blockquote = function (event) { return event.altKey && event.shiftKey && event.keyCode === 87; }; // w
      scribe.use(scribePluginBlockquoteCommand());
      tags.blockquote = {id:true};
    }

    // Headings
    if (options.multiline && options.formatting.indexOf('heading') !== -1) {
      keyCommands.h3 = function (event) { return ctrlKey(event) && event.keyCode === 50; }; // 2
      keyCommands.h4 = function (event) { return ctrlKey(event) && event.keyCode === 51; }; // 2
      scribe.use(scribePluginHeadingCommand(3));
      scribe.use(scribePluginHeadingCommand(4));
      tags.h3 = {id:true};
      tags.h4 = {id:true};
    }

    // Inline Objects
    if (options.multiline && options.inlineObjects) {
      scribe.use(scribePluginInlineObjects(options.inlineObjects));

      // Maybe make optionally load these similar to formatting. For now, it's an all or nothing.

      scribe.use(scribePluginBettyCropper(options.image));
      scribe.use(scribePluginYoutube());
      scribe.use(scribePluginEmbed());
      scribe.use(scribePluginEmbedInstagram());
      scribe.use(scribePluginHr());
      scribe.use(scribePluginAnchor());
      scribe.use(scribePluginOnionVideo(options.video));
      scribe.use(removeAStyles());
      scribe.use(stripBoldInHeadings());
    }

    scribe.use(scribePluginSanitizer({
      tags: tags,
      skipSanitization: skipSanitization
    }));
    scribe.use(pasteFromWord());
    scribe.use(pasteSanitize());
    scribe.use(pasteStripNewlines());
    scribe.use(pasteStripNbsps());
    // Word count

    if (options.statsContainer) {
      setInterval(function () {
        $(options.statsContainer).html(
          $(scribe.el).text().split(' ').length
        );
      }, 3000);
    }

    // Per-Element Export Filtering
    scribe.use(filterForExport());

    /* This is necessary for a few dumb reasons. Scribe's transaction manager doesn't work when there
      ins't a selection inside of the editor. This means any changes made when the editor ins't in focus,
      like adding an image, stuff breaks. This works around that particular issue.

      I'm not really sure the right way to fix this or how to avoid this problem.

      The scroll stuff is a consequence of this.
    */
    scribe.updateContents = function(fn, skipFormatters) {
      // Default is to skipFormatters. Only place this needs to be set to false is when updating links.
      // We want formatters to run on links. Embeds & other shit seem to get sanitized
      // despite there being safegaurds for that.
      if (typeof skipFormatters === 'undefined') {
        skipFormatters = true;
      }
      scribe._skipFormatters = skipFormatters;
      var scrollY = window.scrollY;
      setTimeout(function() {
        scribe.el.focus();
        setTimeout(function() {
          scribe.transactionManager.run(fn);
          window.scrollTo(0, scrollY);

          // This should notify any changes that happen outside of typing
          scribe.trigger('content-changed');
        }, 20);
      }, 20);
    };

    scribe.use(scribePluginCurlyQuotes());
    scribe.use(scribePluginKeyboardShortcuts(Object.freeze(keyCommands)));

    // garbage toolbar setup
    if (options.multiline) {
      var toolbarSelector = '.document-tools .toolbar-contents';
      var $toolbar = $(scribe.el.parentNode).find(toolbarSelector);

      scribe.use(scribePluginToolbar($toolbar[0], options));

      $toolbar.hide();
      $(scribe.el)
        .on('focus', function () {
          $toolbar.show();
          $(toolbarSelector).not($toolbar).hide();
        });
    } else {
      $(element.parentNode).find('.document-tools .toolbar-contents').hide();
    }

    // a little hacky to prevent deletion of images and other inline elements via the backspace key.
    scribe.el.addEventListener('keydown', function(event) {
      if (event.keyCode === 8) {
        // is the previous immediate child of editor an inline item?
        var sel = new scribe.api.Selection();
        var prev = $(sel.selection.anchorNode).closest('.editor>*').prev();
        if (prev.hasClass('inline')
          && sel.selection.anchorOffset === 0
          && sel.selection.isCollapsed) {
          event.preventDefault();
        }
      }
    });

    scribe.use(scribePluginFormatterPlainTextConvertNewLinesToHtml());

    this.setChangeHandler = function(func) {
      scribe.on('content-changed', func);
    };

    this.setContent = function(content) {
      if (!content) {
          content = '

';
      }
      scribe.setContent(content);
    };

    this.getContent = function() {
      //todo: if multiline is false, only return contents of the paragraph

      var contents = scribe.getContent();

      // Allow any plugins to clean up markup. Main use case is for embed plugin, atm.
      return contents;
    };

    this.setupToolbar = setupToolbar;

    this.scribe = scribe;
    return this;
  }

  return OnionEditor;

});
<|endoftext|>