<commit_before>import Promise from 'bluebird'
import React, {Component, PropTypes} from 'react'
import {connect} from 'react-redux'
import { actions, selectors } from 'redux/editor'
import * as entities from 'redux/entities'
import Preview from '../../components/Preview/Preview.js'
import EntityTreeBox from '../../components/EntityTree/EntityTreeBox.js'
import EntityTree from '../../components/EntityTree/EntityTree.js'
import Properties from '../../components/Properties/Properties.js'
import style from './App.scss'
import Toolbar from '../../components/Toolbar/Toolbar.js'
import Helmet from 'react-helmet'
import SplitPane from '../../components/common/SplitPane/SplitPane.js'
import EditorTabs from '../../components/Tabs/EditorTabs.js'
import TabTitles from '../../components/Tabs/TabTitles.js'
import Modal from '../Modal/Modal.js'
import NewEntityModal from '../../components/Modals/NewEntityModal.js'
import DeleteConfirmationModal from '../../components/Modals/DeleteConfirmationModal.js'
import CloseConfirmationModal from '../../components/Modals/CloseConfirmationModal.js'
import RenameModal from '../../components/Modals/RenameModal.js'
import RestoreDockConfirmationModal from '../../components/Modals/RestoreDockConfirmationModal.js'
import * as progress from '../../redux/progress'
import cookies from 'js-cookie'
import {
  triggerSplitResize,
  removeHandler,
  previewListeners,
  registerPreviewHandler,
  entitySets,
  shouldOpenStartupPage,
  registerCollapseLeftHandler,
  registerCollapsePreviewHandler,
  entityTreeWrapperComponents
} from '../../lib/configuration.js'

const progressActions = progress.actions

@connect((state) => ({
  entities: state.entities,
  references: entities.selectors.getReferences(state),
  activeTabKey: state.editor.activeTabKey,
  activeTabWithEntity: selectors.getActiveTabWithEntity(state),
  isPending: progress.selectors.getIsPending(state),
  canRun: selectors.canRun(state),
  canSave: selectors.canSave(state),
  canSaveAll: selectors.canSaveAll(state),
  canReformat: selectors.canReformat(state),
  tabsWithEntities: selectors.getTabWithEntities(state),
  activeEntity: selectors.getActiveEntity(state),
  lastActiveTemplate: selectors.getLastActiveTemplate(state),
  undockMode: state.editor.undockMode
}), { ...actions, ...progressActions })
export default class App extends Component {
  static contextTypes = {
    store: PropTypes.object.isRequired
  }

  static propTypes = {
    entities: PropTypes.object,
    references: PropTypes.object,
    tabsWithEntities: PropTypes.array,
    currentDetail: PropTypes.object,
    error: PropTypes.string,
    loading: PropTypes.bool,
    loaded: PropTypes.bool
  };

  constructor (props) {
    super(props)

    this.previews = {}

    this.openModal = this.openModal.bind(this)
    this.handlePreviewCollapsing = this.handlePreviewCollapsing.bind(this)
    this.handlePreviewDocking = this.handlePreviewDocking.bind(this)
    this.handlePreviewUndocking = this.handlePreviewUndocking.bind(this)
    this.handlePreviewUndocked = this.handlePreviewUndocked.bind(this)
    this.isPreviewUndockeable = this.isPreviewUndockeable.bind(this)
  }

  componentDidMount () {
    window.onbeforeunload = () => {
      if (this.props.canSaveAll) {
        return 'You may have unsaved changes'
      }
    }

    registerPreviewHandler((src) => {
      if (!src) {
        this.handleRun(undefined, this.props.undockMode)
      }
    })

    registerCollapseLeftHandler((type = true) => {
      this.refs.leftPane.collapse(type)
    })

    registerCollapsePreviewHandler((type = true) => {
      this.refs.previewPane.collapse(type)
    })

    previewListeners.push((request, entities, target) => {
      const { undockMode } = this.props

      // we need to try to open the window again to get a reference to any existing window
      // created with the id, this is necessary because the window can be closed by the user
      // using the native close functionality of the browser tab,
      // if we don't try to open the window again we will have inconsistent references and
      // we can not close all preview tabs when un-collapsing the main pane preview again
      if (undockMode && this.refs.previewPane && target && target.indexOf(request.template.shortid) !== -1) {
        let previewWinOpts = this.getPreviewWindowOptions()
        this.previews[previewWinOpts.id] = this.refs.previewPane.openWindow(previewWinOpts)
      }
    })

    if (this.props.params.shortid) {
      this.props.openTab({ shortid: this.props.params.shortid, entitySet: this.props.params.entitySet })
      return
    }

    this.openStartup()
  }

  componentDidUpdate () {
    this.props.updateHistory()
  }

  async handleRun (target, undockMode) {
    this.props.start()
    cookies.set('render-complete', false)

    const interval = setInterval(() => {
      if (cookies.get('render-complete') === 'true') {
        clearInterval(interval)
        this.props.stop()
      }
    }, 1000)

    if (undockMode) {
      let previewWindowOpts = this.getPreviewWindowOptions()

      this.props.run(previewWindowOpts.name)
      return
    }

    this.props.run(target)
  }

  openModal (componentOrText, options) {
    this.refOpenModal(componentOrText, options)
  }

  getPreviewWindowOptions () {
    const { activeTabWithEntity } = this.props

    return {
      id: activeTabWithEntity.entity.shortid,
      name: 'previewFrame-' + activeTabWithEntity.entity.shortid,
      tab: true
    }
  }

  save () {
    return this.props.save()
  }

  saveAll () {
    return this.props.saveAll()
  }

  handleSplitChanged () {
    triggerSplitResize()

    if (this.refs.preview) {
      this.refs.preview.resizeStarted()
    }
  }

  openStartup () {
    if (shouldOpenStartupPage) {
      this.props.openTab({ key: 'StartupPage', editorComponentKey: 'startup', title: 'Startup' })
    }
  }

  closeTab (key) {
    const entity = entities.selectors.getById(this.context.store.getState(), key, false)
    if (!entity || !entity.__isDirty) {
      return this.props.closeTab(key)
    }
    this.openModal(CloseConfirmationModal, { _id: key })
  }

  handleSplitDragFinished () {
    if (this.refs.preview) {
      this.refs.preview.resizeEnded()
    }
  }

  isPreviewUndockeable () {
    const { activeTabWithEntity, undockMode } = this.props

    // if we are in undock mode the pane return true
    if (undockMode) {
      return true
    }

    return (
      activeTabWithEntity &&
      activeTabWithEntity.entity &&
      activeTabWithEntity.entity.__entitySet === 'templates'
    )
  }

  handlePreviewCollapsing (collapsed) {
    if (!this.props.undockMode) {
      return true
    }

    if (!collapsed) {
      return new Promise((resolve) => {
        this.openModal(RestoreDockConfirmationModal, {
          onResponse: (response) => resolve(response)
        })
      })
    }

    return true
  }

  handlePreviewDocking () {
    // close all preview windows when docking
    if (Object.keys(this.previews).length) {
      Object.keys(this.previews)
      .forEach((id) => this.previews[id] && this.previews[id].close())
    }

    this.previews = {}
  }

  handlePreviewUndocking () {
    const { activeTabWithEntity } = this.props

    if (
      activeTabWithEntity &&
      activeTabWithEntity.entity &&
      activeTabWithEntity.entity.__entitySet === 'templates'
    ) {
      this.props.activateUndockMode()

      return this.getPreviewWindowOptions()
    }

    return false
  }

  handlePreviewUndocked (id, previewWindow) {
    this.previews[id] = previewWindow

    this.handleRun(undefined, this.props.undockMode)
  }

  renderEntityTree () {
    const containerStyles = {
      display: 'flex',
      flex: 1,
      flexDirection: 'column',
      // firefox needs min-height and min-width explicitly declared to allow descendants flex items to be scrollable (overflow)
      minWidth: 0,
      minHeight: 0
    }

    const { activeEntity, references, openTab } = this.props

    const entityTreeProps = {
      toolbar: true,
      onRename: (id) => this.openModal(RenameModal, { _id: id }),
      onClone: (entity) => {
        this.openModal(NewEntityModal, {
          entity: entity,
          entitySet: entity.__entitySet,
          initialName: entity.name + '(clone)'
        })
      },
      onRemove: (id) => removeHandler ? removeHandler(id) : this.openModal(DeleteConfirmationModal, {_id: id}),
      activeEntity,
      entities: references,
      onClick: (_id) => openTab({_id: _id}),
      onNewClick: (es) => entitySets[es].onNew ? entitySets[es].onNew() : this.openModal(NewEntityModal, {entitySet: es})
    }

    // if there are no components registered, defaults to rendering the EntityTree alone
    if (!entityTreeWrapperComponents.length) {
      return React.createElement(EntityTree, entityTreeProps)
    }

    // composing components
    const wrappedEntityTree = entityTreeWrapperComponents.reduce((prevElement, component) => {
      const propsToWrapper = {
        entities: references,
        entitySets: entitySets,
        containerStyles
      }

      if (prevElement == null) {
        return React.createElement(
          component,
          propsToWrapper,
          React.createElement(EntityTree, entityTreeProps)
        )
      }

      return React.createElement(
        component,
        propsToWrapper,
        prevElement
      )
    }, null)

    if (!wrappedEntityTree) {
      return null
    }

    return wrappedEntityTree
  }

  render () {
    const {
      tabsWithEntities,
      isPending,
      canRun,
      canSave,
      canSaveAll,
      canReformat,
      activeTabWithEntity,
      entities,
      stop,
      activateTab,
      activeTabKey,
      activeEntity,
      update,
      groupedUpdate,
      reformat,
      undockMode
    } = this.props

    return (

        <Helmet />
        <Modal ref='modal' openCallback={(open) => { this.refOpenModal = open }} />

            <Toolbar
              canRun={canRun} canSave={canSave} canSaveAll={canSaveAll} onSave={() => this.save()}
              onSaveAll={() => this.saveAll()} isPending={isPending} activeTab={activeTabWithEntity} onUpdate={update}
              canReformat={canReformat} onReformat={reformat}
              onRun={(target, ignoreUndockMode) => this.handleRun(target, ignoreUndockMode ? false : undockMode)} openStartup={() => this.openStartup()} />

              <SplitPane
                ref='leftPane'
                collapsedText='Objects / Properties' collapsable='first'
                resizerClassName='resizer' defaultSize='85%' onChange={() => this.handleSplitChanged()}
                onDragFinished={() => this.handleSplitDragFinished()}>
                <SplitPane
                  resizerClassName='resizer-horizontal' split='horizontal'
                  defaultSize={(window.innerHeight * 0.5) + 'px'}>
                  <EntityTreeBox>
                    {this.renderEntityTree()}
                  </EntityTreeBox>
                  <Properties entity={activeEntity} entities={entities} onChange={update} />
                </SplitPane>

                  <TabTitles
                    activeTabKey={activeTabKey} activateTab={activateTab} tabs={tabsWithEntities}
                    closeTab={(k) => this.closeTab(k)} />
                  <SplitPane
                    ref='previewPane'
                    collapsedText='preview'
                    collapsable='second'
                    undockeable={this.isPreviewUndockeable}
                    onChange={() => this.handleSplitChanged()}
                    onCollapsing={this.handlePreviewCollapsing}
                    onDocking={this.handlePreviewDocking}
                    onUndocking={this.handlePreviewUndocking}
                    onUndocked={this.handlePreviewUndocked}
                    onDragFinished={() => this.handleSplitDragFinished()}
                    resizerClassName='resizer'>
                    <EditorTabs
                      activeTabKey={activeTabKey} onUpdate={(v) => groupedUpdate(v)} tabs={tabsWithEntities} />
                    <Preview ref='preview' onLoad={stop} />
                  </SplitPane>

              </SplitPane>

    )
  }
}
<commit_msg>cloning extensions with separator should be named diferently<commit_after>import Promise from 'bluebird'
import React, {Component, PropTypes} from 'react'
import {connect} from 'react-redux'
import { actions, selectors } from 'redux/editor'
import * as entities from 'redux/entities'
import Preview from '../../components/Preview/Preview.js'
import EntityTreeBox from '../../components/EntityTree/EntityTreeBox.js'
import EntityTree from '../../components/EntityTree/EntityTree.js'
import Properties from '../../components/Properties/Properties.js'
import style from './App.scss'
import Toolbar from '../../components/Toolbar/Toolbar.js'
import Helmet from 'react-helmet'
import SplitPane from '../../components/common/SplitPane/SplitPane.js'
import EditorTabs from '../../components/Tabs/EditorTabs.js'
import TabTitles from '../../components/Tabs/TabTitles.js'
import Modal from '../Modal/Modal.js'
import NewEntityModal from '../../components/Modals/NewEntityModal.js'
import DeleteConfirmationModal from '../../components/Modals/DeleteConfirmationModal.js'
import CloseConfirmationModal from '../../components/Modals/CloseConfirmationModal.js'
import RenameModal from '../../components/Modals/RenameModal.js'
import RestoreDockConfirmationModal from '../../components/Modals/RestoreDockConfirmationModal.js'
import * as progress from '../../redux/progress'
import cookies from 'js-cookie'
import {
  triggerSplitResize,
  removeHandler,
  previewListeners,
  registerPreviewHandler,
  entitySets,
  shouldOpenStartupPage,
  registerCollapseLeftHandler,
  registerCollapsePreviewHandler,
  entityTreeWrapperComponents
} from '../../lib/configuration.js'

const progressActions = progress.actions

@connect((state) => ({
  entities: state.entities,
  references: entities.selectors.getReferences(state),
  activeTabKey: state.editor.activeTabKey,
  activeTabWithEntity: selectors.getActiveTabWithEntity(state),
  isPending: progress.selectors.getIsPending(state),
  canRun: selectors.canRun(state),
  canSave: selectors.canSave(state),
  canSaveAll: selectors.canSaveAll(state),
  canReformat: selectors.canReformat(state),
  tabsWithEntities: selectors.getTabWithEntities(state),
  activeEntity: selectors.getActiveEntity(state),
  lastActiveTemplate: selectors.getLastActiveTemplate(state),
  undockMode: state.editor.undockMode
}), { ...actions, ...progressActions })
export default class App extends Component {
  static contextTypes = {
    store: PropTypes.object.isRequired
  }

  static propTypes = {
    entities: PropTypes.object,
    references: PropTypes.object,
    tabsWithEntities: PropTypes.array,
    currentDetail: PropTypes.object,
    error: PropTypes.string,
    loading: PropTypes.bool,
    loaded: PropTypes.bool
  };

  constructor (props) {
    super(props)

    this.previews = {}

    this.openModal = this.openModal.bind(this)
    this.handlePreviewCollapsing = this.handlePreviewCollapsing.bind(this)
    this.handlePreviewDocking = this.handlePreviewDocking.bind(this)
    this.handlePreviewUndocking = this.handlePreviewUndocking.bind(this)
    this.handlePreviewUndocked = this.handlePreviewUndocked.bind(this)
    this.isPreviewUndockeable = this.isPreviewUndockeable.bind(this)
  }

  componentDidMount () {
    window.onbeforeunload = () => {
      if (this.props.canSaveAll) {
        return 'You may have unsaved changes'
      }
    }

    registerPreviewHandler((src) => {
      if (!src) {
        this.handleRun(undefined, this.props.undockMode)
      }
    })

    registerCollapseLeftHandler((type = true) => {
      this.refs.leftPane.collapse(type)
    })

    registerCollapsePreviewHandler((type = true) => {
      this.refs.previewPane.collapse(type)
    })

    previewListeners.push((request, entities, target) => {
      const { undockMode } = this.props

      // we need to try to open the window again to get a reference to any existing window
      // created with the id, this is necessary because the window can be closed by the user
      // using the native close functionality of the browser tab,
      // if we don't try to open the window again we will have inconsistent references and
      // we can not close all preview tabs when un-collapsing the main pane preview again
      if (undockMode && this.refs.previewPane && target && target.indexOf(request.template.shortid) !== -1) {
        let previewWinOpts = this.getPreviewWindowOptions()
        this.previews[previewWinOpts.id] = this.refs.previewPane.openWindow(previewWinOpts)
      }
    })

    if (this.props.params.shortid) {
      this.props.openTab({ shortid: this.props.params.shortid, entitySet: this.props.params.entitySet })
      return
    }

    this.openStartup()
  }

  componentDidUpdate () {
    this.props.updateHistory()
  }

  async handleRun (target, undockMode) {
    this.props.start()
    cookies.set('render-complete', false)

    const interval = setInterval(() => {
      if (cookies.get('render-complete') === 'true') {
        clearInterval(interval)
        this.props.stop()
      }
    }, 1000)

    if (undockMode) {
      let previewWindowOpts = this.getPreviewWindowOptions()

      this.props.run(previewWindowOpts.name)
      return
    }

    this.props.run(target)
  }

  openModal (componentOrText, options) {
    this.refOpenModal(componentOrText, options)
  }

  getPreviewWindowOptions () {
    const { activeTabWithEntity } = this.props

    return {
      id: activeTabWithEntity.entity.shortid,
      name: 'previewFrame-' + activeTabWithEntity.entity.shortid,
      tab: true
    }
  }

  save () {
    return this.props.save()
  }

  saveAll () {
    return this.props.saveAll()
  }

  handleSplitChanged () {
    triggerSplitResize()

    if (this.refs.preview) {
      this.refs.preview.resizeStarted()
    }
  }

  openStartup () {
    if (shouldOpenStartupPage) {
      this.props.openTab({ key: 'StartupPage', editorComponentKey: 'startup', title: 'Startup' })
    }
  }

  closeTab (key) {
    const entity = entities.selectors.getById(this.context.store.getState(), key, false)
    if (!entity || !entity.__isDirty) {
      return this.props.closeTab(key)
    }
    this.openModal(CloseConfirmationModal, { _id: key })
  }

  handleSplitDragFinished () {
    if (this.refs.preview) {
      this.refs.preview.resizeEnded()
    }
  }

  isPreviewUndockeable () {
    const { activeTabWithEntity, undockMode } = this.props

    // if we are in undock mode the pane return true
    if (undockMode) {
      return true
    }

    return (
      activeTabWithEntity &&
      activeTabWithEntity.entity &&
      activeTabWithEntity.entity.__entitySet === 'templates'
    )
  }

  handlePreviewCollapsing (collapsed) {
    if (!this.props.undockMode) {
      return true
    }

    if (!collapsed) {
      return new Promise((resolve) => {
        this.openModal(RestoreDockConfirmationModal, {
          onResponse: (response) => resolve(response)
        })
      })
    }

    return true
  }

  handlePreviewDocking () {
    // close all preview windows when docking
    if (Object.keys(this.previews).length) {
      Object.keys(this.previews)
      .forEach((id) => this.previews[id] && this.previews[id].close())
    }

    this.previews = {}
  }

  handlePreviewUndocking () {
    const { activeTabWithEntity } = this.props

    if (
      activeTabWithEntity &&
      activeTabWithEntity.entity &&
      activeTabWithEntity.entity.__entitySet === 'templates'
    ) {
      this.props.activateUndockMode()

      return this.getPreviewWindowOptions()
    }

    return false
  }

  handlePreviewUndocked (id, previewWindow) {
    this.previews[id] = previewWindow

    this.handleRun(undefined, this.props.undockMode)
  }

  renderEntityTree () {
    const containerStyles = {
      display: 'flex',
      flex: 1,
      flexDirection: 'column',
      // firefox needs min-height and min-width explicitly declared to allow descendants flex items to be scrollable (overflow)
      minWidth: 0,
      minHeight: 0
    }

    const { activeEntity, references, openTab } = this.props

    const entityTreeProps = {
      toolbar: true,
      onRename: (id) => this.openModal(RenameModal, { _id: id }),
      onClone: (entity) => {
        this.openModal(NewEntityModal, {
          entity: entity,
          entitySet: entity.__entitySet,
          initialName: entity.name.indexOf('.') !== -1 ? entity.name.slice(0, entity.name.indexOf('.')) + '(clone)' + entity.name.slice(entity.name.indexOf('.')) : entity.name + '(clone)'
        })
      },
      onRemove: (id) => removeHandler ? removeHandler(id) : this.openModal(DeleteConfirmationModal, {_id: id}),
      activeEntity,
      entities: references,
      onClick: (_id) => openTab({_id: _id}),
      onNewClick: (es) => entitySets[es].onNew ? entitySets[es].onNew() : this.openModal(NewEntityModal, {entitySet: es})
    }

    // if there are no components registered, defaults to rendering the EntityTree alone
    if (!entityTreeWrapperComponents.length) {
      return React.createElement(EntityTree, entityTreeProps)
    }

    // composing components
    const wrappedEntityTree = entityTreeWrapperComponents.reduce((prevElement, component) => {
      const propsToWrapper = {
        entities: references,
        entitySets: entitySets,
        containerStyles
      }

      if (prevElement == null) {
        return React.createElement(
          component,
          propsToWrapper,
          React.createElement(EntityTree, entityTreeProps)
        )
      }

      return React.createElement(
        component,
        propsToWrapper,
        prevElement
      )
    }, null)

    if (!wrappedEntityTree) {
      return null
    }

    return wrappedEntityTree
  }

  render () {
    const {
      tabsWithEntities,
      isPending,
      canRun,
      canSave,
      canSaveAll,
      canReformat,
      activeTabWithEntity,
      entities,
      stop,
      activateTab,
      activeTabKey,
      activeEntity,
      update,
      groupedUpdate,
      reformat,
      undockMode
    } = this.props

    return (

        <Helmet />
        <Modal ref='modal' openCallback={(open) => { this.refOpenModal = open }} />

            <Toolbar
              canRun={canRun} canSave={canSave} canSaveAll={canSaveAll} onSave={() => this.save()}
              onSaveAll={() => this.saveAll()} isPending={isPending} activeTab={activeTabWithEntity} onUpdate={update}
              canReformat={canReformat} onReformat={reformat}
              onRun={(target, ignoreUndockMode) => this.handleRun(target, ignoreUndockMode ? false : undockMode)} openStartup={() => this.openStartup()} />

              <SplitPane
                ref='leftPane'
                collapsedText='Objects / Properties' collapsable='first'
                resizerClassName='resizer' defaultSize='85%' onChange={() => this.handleSplitChanged()}
                onDragFinished={() => this.handleSplitDragFinished()}>
                <SplitPane
                  resizerClassName='resizer-horizontal' split='horizontal'
                  defaultSize={(window.innerHeight * 0.5) + 'px'}>
                  <EntityTreeBox>
                    {this.renderEntityTree()}
                  </EntityTreeBox>
                  <Properties entity={activeEntity} entities={entities} onChange={update} />
                </SplitPane>

                  <TabTitles
                    activeTabKey={activeTabKey} activateTab={activateTab} tabs={tabsWithEntities}
                    closeTab={(k) => this.closeTab(k)} />
                  <SplitPane
                    ref='previewPane'
                    collapsedText='preview'
                    collapsable='second'
                    undockeable={this.isPreviewUndockeable}
                    onChange={() => this.handleSplitChanged()}
                    onCollapsing={this.handlePreviewCollapsing}
                    onDocking={this.handlePreviewDocking}
                    onUndocking={this.handlePreviewUndocking}
                    onUndocked={this.handlePreviewUndocked}
                    onDragFinished={() => this.handleSplitDragFinished()}
                    resizerClassName='resizer'>
                    <EditorTabs
                      activeTabKey={activeTabKey} onUpdate={(v) => groupedUpdate(v)} tabs={tabsWithEntities} />
                    <Preview ref='preview' onLoad={stop} />
                  </SplitPane>

              </SplitPane>

    )
  }
}
<|endoftext|>


<commit_before>/*
 * Class: FlightDeck
 * Initializes all needed functionality
 */

var FlightDeck = new Class({
    Implements: [Options, Events],
    options: {
        menu_el: 'UI_Editor_Menu',
        try_in_browser_class: 'XPI_test',
        xpi_hashtag: '',        // hashtag for the XPI creation
        max_request_number: 50, // how many times should system try to download XPI
        request_interval: 5000  // try to download XPI every 5 sec
        //user: ''
    },
    initialize: function() {
        this.uri = new URI();
        if (this.uri.getData('redirect','fragment')) {
            window.location.href = this.uri.getData('redirect', 'fragment');
        }
        this.warning = this.error = this.message = {
            'alert': function(title, message) {
                alert(title+"\n"+message);
            }
        };
        this.editors = [];
        this.parseTooltips();
        this.createActionSections();
        this.parseTestButtons.bind(this).delay(10);
        this.addEvent('xpi_installed', this.whenXpiInstalled);
        this.addEvent('xpi_uninstalled', this.whenXpiUninstalled);
        if (!this.isAddonInstalled()) $log('FD: No Addon Builder Helper')
    },

    parseTooltips: function() {
        this.tips = new Tips({
            fixed: false,
            className: 'UI_tooltip',
            offset: {
                x: 0,
                y: 16
            }
        });

        $$('div.UI_tooltip_source').each(function(tipSource){
            tipSource.hide();
            var target = $(tipSource.get('data-tooltip-for'));
            target.set('title', '');
            target.store('tip:title', tipSource.get('data-tooltip-title'));
            target.store('tip:text', tipSource.get('html'));
            this.tips.attach(target);
        }, this);
    },

    setURIRedirect: function(url) {
        // change the URL add #/path/to/saved/revision
        if (this.uri.get('directory') != url) {
            this.uri.setData({'redirect': url}, false, 'fragment');
            this.uri.go();
        }
    },

    whenXpiInstalled: function(name) {
        this.parseTestButtons();
        this.message.alert('Add-ons Builder', '{name} installed'.substitute({'name': [name, 'Add-on'].pick()}));
        // remove SDK from disk
        if (this.rm_xpi_url) {
            new Request.JSON({
                url: this.rm_xpi_url,
                onSuccess: function() {
                    this.fireEvent('sdk_removed');
                }.bind(this)
            }).send();
            this.rm_xpi_url = undefined;
        }
    },

    whenXpiUninstalled: function() {
        this.parseTestButtons();
        this.message.alert('Add-ons Builder', 'Add-on uninstalled');
    },

    /*
    Method: whenAddonInstalled
    create listener for a callback function
     */
    whenAddonInstalled: function(callback) {
        var removeListener = function() {
            document.body.removeEventListener('addonbuilderhelperstart', callback, false);
        }
        document.body.addEventListener('addonbuilderhelperstart', callback, false);
        (function() {
            $log('FD: Warning: not listening to addonbuilderhelperstart, is Helper installed?');
            removeListener();
        }).delay(100000);
    },

    parseTestButtons: function() {
        var installed = (this.isAddonInstalled()) ? this.isXpiInstalled() : false;
        if (installed) {
            $$('.{try_in_browser_class} a'.substitute(this.options)).each(function(test_button){
                if (installed && installed.installedID == test_button.get('rel')) {
                    test_button.getParent('li').addClass('pressed');
                } else {
                    test_button.getParent('li').removeClass('pressed');
                }
            }, this);
        }
    },

    /*
     * Method: testXPI
     */
    testXPI: function(response) {
        $log('FD: DEBUG: XPI delayed ... try to load every ' + this.options.request_interval/1000 + ' seconds' );
        this.request_number = 0;
        this.install_ID = this.tryInstallXPI.periodical(
                this.options.request_interval, this);
        //}
    },

    isXpiInstalled: function() {
        return window.mozFlightDeck.send({cmd:'isInstalled'});
    },

    /*
     * Method: hideEditors
     */
    hideEditors: function() {
        this.editors.each(function(ed){ ed.hide(); });
    },
    /*
     * Method: tryInstallXPI
     *
     * Try to download XPI
     * if successful - stop periodical
     */
    tryInstallXPI: function() {
        if (fd.alertIfNoAddOn()) {
            if (!this.install_xpi_request || (this.install_xpi_request && !this.install_xpi_request.isRunning())) {
                this.request_number += 1;
                url = '/xpi/test/'+this.options.xpi_hashtag+'/';
                $log('FD: installing from ' + url);
                this.install_xpi_request = new Request({
                    url: url,
                    headers: {'Content-Type': 'text/plain; charset=x-user-defined'},
                    onSuccess: function(responseText) {
                        if (responseText || this.request_number > 50) {
                            clearInterval(this.install_ID);
                        }
                        if (responseText) {
                            var result = window.mozFlightDeck.send({cmd: "install", contents: responseText});
                            if (result && result.success) {
                                this.fireEvent('xpi_installed', '');
                            } else {
                                if (result) $log(result);
                                this.warning.alert(
                                    'Add-ons Builder',
                                    'Wrong response from Add-ons Helper. Please let us know'
                                );
                            }
                        }
                    }.bind(this)
                }).send();
            } else {
                $log('FD: DEBUG request is running');
            }
        }
    },
    /*
     * Method: uninstallXPI
     *
     * Remove Add-on from Browser
     */
    uninstallXPI: function(jid) {
        $log('FD: uninstalling ' + jid);
        var result = window.mozFlightDeck.send({cmd:'uninstall'});
        if (result.success) this.fireEvent('xpi_uninstalled');
    },
    /*
     * Method: enableMenuButtons
     */
    enableMenuButtons: function() {
        $$('.' + this.options.menu_el + ' li').each(function(menuItem){
            if (menuItem.hasClass('disabled')){
                menuItem.removeClass('disabled');
            }
        });
    },

    isAddonInstalled: function() {
        return (window.mozFlightDeck) ? true : false;
    },

    /*
     * Method: alertIfNoAddOn
     */
    alertIfNoAddOn: function(callback, text, title) {
        if (this.isAddonInstalled()) return true;
        text = [text,
                "To test this add-on, please install the Add-ons Builder Helper add-on".substitute(settings)].pick();
        title = [title, "Install Add-ons Builder Helper"].pick();
        fd.warning.alert(title, text);
        return false;
    },
    /*
     * Method: createActionSections
     */
    createActionSections: function(){
        $$('.UI_Editor_Menu_Separator').each(function(separator){
            separator.getPrevious('li').addClass('UI_Section_Close');
            separator.getNext('li').addClass('UI_Section_Open');
        });

        var UI_Editor_Menu_Button = $$('.UI_Editor_Menu_Button');

        if (UI_Editor_Menu_Button.length === 1){
            UI_Editor_Menu_Button[0].addClass('UI_Single');
        }
    }
});

/*
 * Add delay do Spinner
 */

Spinner = Class.refactor(Spinner, {
    options: { delay: 400 },
});

/*
 * Default onFailure in all Requests
 */

Request = Class.refactor(Request, {
    options: {
        onFailure: function(xhr) {
            if (this.options.addOnFailure) {
              this.options.addOnFailure();
            }
            fd.error.alert(
                'Error {status}'.substitute(xhr),
                '{statusText}
{responseText}'.substitute(xhr)
                );
        }
    },
    initialize: function(options) {
      this.previous(options);
      // It happened to be unnecessary
      //if (csrfmiddlewaretoken && (this.options.method == 'post' || this.options.method == 'POST')) {
      //  this.options.data['csrfmiddlewaretoken'] = csrfmiddlewaretoken;
      //}

    },
  // overloading processScripts to *not* execute JS responses
    processScripts: function(text){
        if (this.options.evalResponse) return Browser.exec(text);
        return text.stripScripts(this.options.evalScripts);
    },
});

/*
 * Inspired by
 * http://github.com/jeresig/sizzle/commit/7631f9c3f85e5fa72ac51532399cb593c2cdc71f
 * and this http://github.com/jeresig/sizzle/commit/5716360040a440041da19823964f96d025ca734b
 * and then http://dev.jquery.com/ticket/4512
 */

Element.implement({

    isHidden: function(){
        var w = this.offsetWidth, h = this.offsetHeight,
        force = (this.tagName.toLowerCase() === 'tr');
        return (w===0 && h===0 && !force)
            ? true
            : (w!==0 && h!==0 && !force) ? false : this.getStyle('display') === 'none';
    },
    isVisible: function(){
        return !this.isHidden();
    },
    getSiblings: function(match,nocache) {
        return this.getParent().getChildren(match,nocache).erase(this);
    }

});

/*
    Add $name mutator - specifies the type of the created Class
    Usage:
        var C = new Class({$name = 'sometype', inititate: function() {}});
        var c = new C();
        alert(typeOf(c)); // 'sometype'
 */
Class.Mutators.$name = function(name){ this.implement('$family', {name: name}); };

/*
 * Add validation for alphanum + "-_."
 */
Form.Validator.addAllThese([
    ['validate-alphanum_plus', {
        errorMsg:     'Please use only letters (a-z),
'+
                    'numbers (0-9) or \"_.-\" only in this field.
'+
                    'No spaces or other characters are allowed.',
        test: function(element){
            return Form.Validator.getValidator('IsEmpty').test(element) ||  (/^[a-zA-Z0-9._\-]+$/).test(element.get('value'));
        }
    }],
    ['validate-alphanum_plus_space', {
        errorMsg:     'Please use only letters (a-z),
'+
                    'numbers (0-9) spaces or \"_().-\" only in this field.
'+
                    'No other characters are allowed.',
        test: function(element){
            return Form.Validator.getValidator('IsEmpty').test(element) ||  (/^[a-zA-Z0-9\ _\(\).\-]+$/).test(element.get('value'));
        }
    }]
]);

(function(){
    var html_symbols = ['&','"','<','>','','','','','','','','','','','','','','','','','','','','','','','','','','','','','',''],
    html_names = ['&','"','<','>','¡','¢','£','¤','¥','¦','§','¨','©','ª','«','¬','®','¯','°','±','²','³','´','µ','¶','·','¸','¹','º','»','¼','½','¾','¿'],
    js_symbols = ['\\(','\\)','\\{','\\}'],
    html_number = ['\(','\)','\{','\}'];

    String.implement({
        escapeHTML: function() {
            text = this;
            html_symbols.each(function(symbol, i){
                text = text.replace(new RegExp(symbol, 'g'), html_names[i]);
            });
            return text;
        },
        escapeJS: function() {
            text = this;
            js_symbols.each(function(symbol, i){
                text = text.replace(new RegExp(symbol, 'g'), html_number[i]);
            });
            return text;
        },
        escapeAll: function() {
            return this.escapeHTML()//.escapeJS();
        }
    });
})();

/*
    Listen to an event fired when Extension is installed
    This wasn't working
window.addEvent('load', function() {
    if (window.mozFlightDeck) {
        window.mozFlightDeck.whenMessaged(function(data) {
            // This gets called when one of our extensions has been installed
            // successfully, or failed somehow.
            fd.message.alert('Add-ons Builder', 'Add-on {msg}'.substitute(data));
            // log to console result of isInstalled command
            $log('sending isInstalled to window.mozFlightDeck');
            $log(fd.isXpiInstalled());
        });
    }
});
 */
<commit_msg>first attempt to download starts after a second from initial request<commit_after>/*
 * Class: FlightDeck
 * Initializes all needed functionality
 */

var FlightDeck = new Class({
    Implements: [Options, Events],
    options: {
        menu_el: 'UI_Editor_Menu',
        try_in_browser_class: 'XPI_test',
        xpi_hashtag: '',        // hashtag for the XPI creation
        max_request_number: 50, // how many times should system try to download XPI
        request_interval: 5000  // try to download XPI every 5 sec
        //user: ''
    },
    initialize: function() {
        this.uri = new URI();
        if (this.uri.getData('redirect','fragment')) {
            window.location.href = this.uri.getData('redirect', 'fragment');
        }
        this.warning = this.error = this.message = {
            'alert': function(title, message) {
                alert(title+"\n"+message);
            }
        };
        this.editors = [];
        this.parseTooltips();
        this.createActionSections();
        this.parseTestButtons.bind(this).delay(10);
        this.addEvent('xpi_installed', this.whenXpiInstalled);
        this.addEvent('xpi_uninstalled', this.whenXpiUninstalled);
        if (!this.isAddonInstalled()) $log('FD: No Addon Builder Helper')
    },

    parseTooltips: function() {
        this.tips = new Tips({
            fixed: false,
            className: 'UI_tooltip',
            offset: {
                x: 0,
                y: 16
            }
        });

        $$('div.UI_tooltip_source').each(function(tipSource){
            tipSource.hide();
            var target = $(tipSource.get('data-tooltip-for'));
            target.set('title', '');
            target.store('tip:title', tipSource.get('data-tooltip-title'));
            target.store('tip:text', tipSource.get('html'));
            this.tips.attach(target);
        }, this);
    },

    setURIRedirect: function(url) {
        // change the URL add #/path/to/saved/revision
        if (this.uri.get('directory') != url) {
            this.uri.setData({'redirect': url}, false, 'fragment');
            this.uri.go();
        }
    },

    whenXpiInstalled: function(name) {
        this.parseTestButtons();
        this.message.alert('Add-ons Builder', '{name} installed'.substitute({'name': [name, 'Add-on'].pick()}));
        // remove SDK from disk
        if (this.rm_xpi_url) {
            new Request.JSON({
                url: this.rm_xpi_url,
                onSuccess: function() {
                    this.fireEvent('sdk_removed');
                }.bind(this)
            }).send();
            this.rm_xpi_url = undefined;
        }
    },

    whenXpiUninstalled: function() {
        this.parseTestButtons();
        this.message.alert('Add-ons Builder', 'Add-on uninstalled');
    },

    /*
    Method: whenAddonInstalled
    create listener for a callback function
     */
    whenAddonInstalled: function(callback) {
        var removeListener = function() {
            document.body.removeEventListener('addonbuilderhelperstart', callback, false);
        }
        document.body.addEventListener('addonbuilderhelperstart', callback, false);
        (function() {
            $log('FD: Warning: not listening to addonbuilderhelperstart, is Helper installed?');
            removeListener();
        }).delay(100000);
    },

    parseTestButtons: function() {
        var installed = (this.isAddonInstalled()) ? this.isXpiInstalled() : false;
        if (installed) {
            $$('.{try_in_browser_class} a'.substitute(this.options)).each(function(test_button){
                if (installed && installed.installedID == test_button.get('rel')) {
                    test_button.getParent('li').addClass('pressed');
                } else {
                    test_button.getParent('li').removeClass('pressed');
                }
            }, this);
        }
    },

    /*
     * Method: testXPI
     */
    testXPI: function(response) {
        $log('FD: DEBUG: XPI delayed ... try to load every ' + this.options.request_interval/1000 + ' seconds' );
        this.request_number = 0;
        this.install_ID = this.tryInstallXPI.delay(1000, this);
        this.install_ID = this.tryInstallXPI.periodical(
                this.options.request_interval, this);
        //}
    },

    isXpiInstalled: function() {
        return window.mozFlightDeck.send({cmd:'isInstalled'});
    },

    /*
     * Method: hideEditors
     */
    hideEditors: function() {
        this.editors.each(function(ed){ ed.hide(); });
    },
    /*
     * Method: tryInstallXPI
     *
     * Try to download XPI
     * if successful - stop periodical
     */
    tryInstallXPI: function() {
        if (fd.alertIfNoAddOn()) {
            if (!this.install_xpi_request || (this.install_xpi_request && !this.install_xpi_request.isRunning())) {
                this.request_number += 1;
                url = '/xpi/test/'+this.options.xpi_hashtag+'/';
                $log('FD: installing from ' + url);
                this.install_xpi_request = new Request({
                    url: url,
                    headers: {'Content-Type': 'text/plain; charset=x-user-defined'},
                    onSuccess: function(responseText) {
                        if (responseText || this.request_number > 50) {
                            clearInterval(this.install_ID);
                        }
                        if (responseText) {
                            var result = window.mozFlightDeck.send({cmd: "install", contents: responseText});
                            if (result && result.success) {
                                this.fireEvent('xpi_installed', '');
                            } else {
                                if (result) $log(result);
                                this.warning.alert(
                                    'Add-ons Builder',
                                    'Wrong response from Add-ons Helper. Please let us know'
                                );
                            }
                        }
                    }.bind(this)
                }).send();
            } else {
                $log('FD: DEBUG request is running');
            }
        }
    },
    /*
     * Method: uninstallXPI
     *
     * Remove Add-on from Browser
     */
    uninstallXPI: function(jid) {
        $log('FD: uninstalling ' + jid);
        var result = window.mozFlightDeck.send({cmd:'uninstall'});
        if (result.success) this.fireEvent('xpi_uninstalled');
    },
    /*
     * Method: enableMenuButtons
     */
    enableMenuButtons: function() {
        $$('.' + this.options.menu_el + ' li').each(function(menuItem){
            if (menuItem.hasClass('disabled')){
                menuItem.removeClass('disabled');
            }
        });
    },

    isAddonInstalled: function() {
        return (window.mozFlightDeck) ? true : false;
    },

    /*
     * Method: alertIfNoAddOn
     */
    alertIfNoAddOn: function(callback, text, title) {
        if (this.isAddonInstalled()) return true;
        text = [text,
                "To test this add-on, please install the Add-ons Builder Helper add-on".substitute(settings)].pick();
        title = [title, "Install Add-ons Builder Helper"].pick();
        fd.warning.alert(title, text);
        return false;
    },
    /*
     * Method: createActionSections
     */
    createActionSections: function(){
        $$('.UI_Editor_Menu_Separator').each(function(separator){
            separator.getPrevious('li').addClass('UI_Section_Close');
            separator.getNext('li').addClass('UI_Section_Open');
        });

        var UI_Editor_Menu_Button = $$('.UI_Editor_Menu_Button');

        if (UI_Editor_Menu_Button.length === 1){
            UI_Editor_Menu_Button[0].addClass('UI_Single');
        }
    }
});

/*
 * Add delay do Spinner
 */

Spinner = Class.refactor(Spinner, {
    options: { delay: 400 },
});

/*
 * Default onFailure in all Requests
 */

Request = Class.refactor(Request, {
    options: {
        onFailure: function(xhr) {
            if (this.options.addOnFailure) {
              this.options.addOnFailure();
            }
            fd.error.alert(
                'Error {status}'.substitute(xhr),
                '{statusText}
{responseText}'.substitute(xhr)
                );
        }
    },
    initialize: function(options) {
      this.previous(options);
      // It happened to be unnecessary
      //if (csrfmiddlewaretoken && (this.options.method == 'post' || this.options.method == 'POST')) {
      //  this.options.data['csrfmiddlewaretoken'] = csrfmiddlewaretoken;
      //}

    },
  // overloading processScripts to *not* execute JS responses
    processScripts: function(text){
        if (this.options.evalResponse) return Browser.exec(text);
        return text.stripScripts(this.options.evalScripts);
    },
});

/*
 * Inspired by
 * http://github.com/jeresig/sizzle/commit/7631f9c3f85e5fa72ac51532399cb593c2cdc71f
 * and this http://github.com/jeresig/sizzle/commit/5716360040a440041da19823964f96d025ca734b
 * and then http://dev.jquery.com/ticket/4512
 */

Element.implement({

    isHidden: function(){
        var w = this.offsetWidth, h = this.offsetHeight,
        force = (this.tagName.toLowerCase() === 'tr');
        return (w===0 && h===0 && !force)
            ? true
            : (w!==0 && h!==0 && !force) ? false : this.getStyle('display') === 'none';
    },
    isVisible: function(){
        return !this.isHidden();
    },
    getSiblings: function(match,nocache) {
        return this.getParent().getChildren(match,nocache).erase(this);
    }

});

/*
    Add $name mutator - specifies the type of the created Class
    Usage:
        var C = new Class({$name = 'sometype', inititate: function() {}});
        var c = new C();
        alert(typeOf(c)); // 'sometype'
 */
Class.Mutators.$name = function(name){ this.implement('$family', {name: name}); };

/*
 * Add validation for alphanum + "-_."
 */
Form.Validator.addAllThese([
    ['validate-alphanum_plus', {
        errorMsg:     'Please use only letters (a-z),
'+
                    'numbers (0-9) or \"_.-\" only in this field.
'+
                    'No spaces or other characters are allowed.',
        test: function(element){
            return Form.Validator.getValidator('IsEmpty').test(element) ||  (/^[a-zA-Z0-9._\-]+$/).test(element.get('value'));
        }
    }],
    ['validate-alphanum_plus_space', {
        errorMsg:     'Please use only letters (a-z),
'+
                    'numbers (0-9) spaces or \"_().-\" only in this field.
'+
                    'No other characters are allowed.',
        test: function(element){
            return Form.Validator.getValidator('IsEmpty').test(element) ||  (/^[a-zA-Z0-9\ _\(\).\-]+$/).test(element.get('value'));
        }
    }]
]);

(function(){
    var html_symbols = ['&','"','<','>','','','','','','','','','','','','','','','','','','','','','','','','','','','','','',''],
    html_names = ['&','"','<','>','¡','¢','£','¤','¥','¦','§','¨','©','ª','«','¬','®','¯','°','±','²','³','´','µ','¶','·','¸','¹','º','»','¼','½','¾','¿'],
    js_symbols = ['\\(','\\)','\\{','\\}'],
    html_number = ['\(','\)','\{','\}'];

    String.implement({
        escapeHTML: function() {
            text = this;
            html_symbols.each(function(symbol, i){
                text = text.replace(new RegExp(symbol, 'g'), html_names[i]);
            });
            return text;
        },
        escapeJS: function() {
            text = this;
            js_symbols.each(function(symbol, i){
                text = text.replace(new RegExp(symbol, 'g'), html_number[i]);
            });
            return text;
        },
        escapeAll: function() {
            return this.escapeHTML()//.escapeJS();
        }
    });
})();

/*
    Listen to an event fired when Extension is installed
    This wasn't working
window.addEvent('load', function() {
    if (window.mozFlightDeck) {
        window.mozFlightDeck.whenMessaged(function(data) {
            // This gets called when one of our extensions has been installed
            // successfully, or failed somehow.
            fd.message.alert('Add-ons Builder', 'Add-on {msg}'.substitute(data));
            // log to console result of isInstalled command
            $log('sending isInstalled to window.mozFlightDeck');
            $log(fd.isXpiInstalled());
        });
    }
});
 */
<|endoftext|>


<commit_before>import {parse} from 'acorn'
import {simple, base} from 'acorn/dist/walk'

import {pack, unpack} from '../value'
import Context from './Context'

/**
 * A Javascript context
 *
 * Implements the Stencila `Context` API. All methods return a Promise.
 *
 * @extends Context
 */
export default class JsContext extends Context {

  constructor (customFunctions = {}) {
    super()
    this._functions = Object.assign({}, customFunctions)
  }

  /**
   * Does the context support a programming language?
   *
   * @override
   */
  supportsLanguage (language) {
    return Promise.resolve(
      language === 'js'
    )
  }

  /**
   * Run JavaScript code
   *
   * @override
   */
  runCode (code = '', options = {}) {
    const pack = (options.pack !== false)

    // Create a function from the code and execute it
    let error = null
    try {
      (new Function(code))() // eslint-disable-line no-new-func
    } catch (err) {
      error = err
    }

    let output
    if (!error) {
      // Evaluate the last line and if no error then make the value output
      // This is inefficient in the sense that the last line is evaluated twice
      // but alternative approaches would appear to require some code parsing
      let lines = code.split('\n')
      let last = lines[lines.length - 1]
      try {
        output = (new Function('return ' + last))() // eslint-disable-line no-new-func
      } catch (err) {
        output = undefined
      }
    }

    return Promise.resolve(
      this._result(error, output, pack)
    )
  }

  /**
   * Call JavaScript code
   *
   * @override
   */
  callCode (code = '', inputs = {}, options = {}) {
    const pack = (options.pack !== false)

    // Extract names and values of inputs
    let names = Object.keys(inputs)
    let values
    if (pack) {
      values = names.map(name => unpack(inputs[name]))
    } else {
      values = names.map(name => inputs[name])
    }

    // Execute the function with the unpacked inputs.
    let error = null
    let output
    try {
      const f = new Function(...names, code) // eslint-disable-line no-new-func
      output = f(...values)
    } catch (err) {
      error = err
    }

    return Promise.resolve(
      this._result(error, output, pack)
    )
  }

  /**
   * Get the dependencies for a piece of code
   *
   * @override
   */
  codeDependencies (code) {
    let names = {}
    let ast = parse(code)
    simple(ast, {
      Identifier: node => {
        let name = node.name
        names[name] = Object.assign(names[name] || {}, {used: true})
      },
      VariableDeclarator: node => {
        let name = node.id.name
        names[name] = Object.assign(names[name] || {}, {declared: true})
      }
    }, base)
    let depends = []
    for (let name in names) { // eslint-disable-line guard-for-in
      let usage = names[name]
      if (usage.used && !usage.declared) depends.push(name)
    }
    return Promise.resolve(depends)
  }

  /**
   * Does the context provide a function?
   *
   * @override
   */
  hasFunction (name) {
    return Boolean(this._functions[name])
  }

  /**
   * Call a function
   *
   * @override
   */
  callFunction (name, args = [], namedArgs = {}, options = {}) {
    if (!name) throw new Error("'name' is mandatory")
    const packing = (options.pack !== false)

    const func = this._functions[name]

    // Convert args into an array of values
    let argValues = []
    if (func.pars) {
      // Unpack args if necessary
      argValues = args.map(arg => packing ? unpack(arg) : arg)
      // Put named arguments into the right place in the argument values array
      for (let name of Object.keys(namedArgs)) {
        let index = func.pars.indexOf(name)
        if (index >-1) {
          let value = namedArgs[name]
          argValues[index] = packing ? unpack(value) : value
        } else {
          return Promise.reject(new Error(`Invalid named argument "${name}"; valid names are ${func.pars.map(name => '"' + name + '"').join(', ')}`))
        }
      }
    } else {
      // Unpack args if necessary
      argValues = args.map(arg => packing ? unpack(arg) : arg)
      // There should be no named arguments since the function does not define any
      if (Object.keys(namedArgs) > 0) return Promise.reject(new Error(`Named arguments supplied but function "${name}" does not support them`))
    }

    let error = null
    let value
    try {
      value = func(...argValues)
    } catch (e) {
      error = e
    }

    return Promise.resolve(
      this._result(error, value, packing)
    )
  }

  /**
   * Return a result promise
   *
   * @param {object} error - Error object, if any
   * @param {object} value - The value to be packed
   * @param {boolean} packed - Should the output be packed (or left unpacked for calls withing Javascript)
   * @return {null|object} - A set of errors by line number
   */
  _result (error, value, packed) {
    if (packed !== false) packed = true

    let errors = null
    if (error) {
      // Parse the error stack to get message and line number
      let lines = error.stack.split('\n')
      let match = lines[1].match(/<anonymous>:(\d+):(\d+)/)
      let line = 0
      let column = 0
      if (match) {
        line = parseInt(match[1], 10) - 1
        column = parseInt(match[2], 10)
      }
      let message = lines[0] || error.message

      errors = [{
        line: line,
        column: column,
        message: message
      }]
    }

    let output
    if (value === undefined) output = null
    else if (packed) output = pack(value)
    else output = value

    return {
      errors: errors,
      output: output
    }
  }

}
<commit_msg>Adds a defineFunction to support dynamic definition of Functions<commit_after>import {parse} from 'acorn'
import {simple, base} from 'acorn/dist/walk'

import {pack, unpack} from '../value'
import Context from './Context'

/**
 * A Javascript context
 *
 * Implements the Stencila `Context` API. All methods return a Promise.
 *
 * @extends Context
 */
export default class JsContext extends Context {

  constructor (customFunctions = {}) {
    super()
    this._functions = Object.assign({}, customFunctions)
  }

  /**
   * Does the context support a programming language?
   *
   * @override
   */
  supportsLanguage (language) {
    return Promise.resolve(
      language === 'js'
    )
  }

  /**
   * Run JavaScript code
   *
   * @override
   */
  runCode (code = '', options = {}) {
    const pack = (options.pack !== false)

    // Create a function from the code and execute it
    let error = null
    try {
      (new Function(code))() // eslint-disable-line no-new-func
    } catch (err) {
      error = err
    }

    let output
    if (!error) {
      // Evaluate the last line and if no error then make the value output
      // This is inefficient in the sense that the last line is evaluated twice
      // but alternative approaches would appear to require some code parsing
      let lines = code.split('\n')
      let last = lines[lines.length - 1]
      try {
        output = (new Function('return ' + last))() // eslint-disable-line no-new-func
      } catch (err) {
        output = undefined
      }
    }

    return Promise.resolve(
      this._result(error, output, pack)
    )
  }

  /**
   * Call JavaScript code
   *
   * @override
   */
  callCode (code = '', inputs = {}, options = {}) {
    const pack = (options.pack !== false)

    // Extract names and values of inputs
    let names = Object.keys(inputs)
    let values
    if (pack) {
      values = names.map(name => unpack(inputs[name]))
    } else {
      values = names.map(name => inputs[name])
    }

    // Execute the function with the unpacked inputs.
    let error = null
    let output
    try {
      const f = new Function(...names, code) // eslint-disable-line no-new-func
      output = f(...values)
    } catch (err) {
      error = err
    }

    return Promise.resolve(
      this._result(error, output, pack)
    )
  }

  /**
   * Get the dependencies for a piece of code
   *
   * @override
   */
  codeDependencies (code) {
    let names = {}
    let ast = parse(code)
    simple(ast, {
      Identifier: node => {
        let name = node.name
        names[name] = Object.assign(names[name] || {}, {used: true})
      },
      VariableDeclarator: node => {
        let name = node.id.name
        names[name] = Object.assign(names[name] || {}, {declared: true})
      }
    }, base)
    let depends = []
    for (let name in names) { // eslint-disable-line guard-for-in
      let usage = names[name]
      if (usage.used && !usage.declared) depends.push(name)
    }
    return Promise.resolve(depends)
  }

  /**
   * Define a function
   *
   * @override
   */
  defineFunction (name, code) {
    this._functions[name] = eval(code) // eslint-disable-line no-eval
    return Promise.resolve()
  }

  /**
   * Does the context provide a function?
   *
   * @override
   */
  hasFunction (name) {
    return Boolean(this._functions[name])
  }

  /**
   * Call a function
   *
   * @override
   */
  callFunction (name, args = [], namedArgs = {}, options = {}) {
    if (!name) throw new Error("'name' is mandatory")
    const packing = (options.pack !== false)

    const func = this._functions[name]

    // Convert args into an array of values
    let argValues = []
    if (func.pars) {
      // Unpack args if necessary
      argValues = args.map(arg => packing ? unpack(arg) : arg)
      // Put named arguments into the right place in the argument values array
      for (let name of Object.keys(namedArgs)) {
        let index = func.pars.indexOf(name)
        if (index >-1) {
          let value = namedArgs[name]
          argValues[index] = packing ? unpack(value) : value
        } else {
          return Promise.reject(new Error(`Invalid named argument "${name}"; valid names are ${func.pars.map(name => '"' + name + '"').join(', ')}`))
        }
      }
    } else {
      // Unpack args if necessary
      argValues = args.map(arg => packing ? unpack(arg) : arg)
      // There should be no named arguments since the function does not define any
      if (Object.keys(namedArgs) > 0) return Promise.reject(new Error(`Named arguments supplied but function "${name}" does not support them`))
    }

    let error = null
    let value
    try {
      value = func(...argValues)
    } catch (e) {
      error = e
    }

    return Promise.resolve(
      this._result(error, value, packing)
    )
  }

  /**
   * Return a result promise
   *
   * @param {object} error - Error object, if any
   * @param {object} value - The value to be packed
   * @param {boolean} packed - Should the output be packed (or left unpacked for calls withing Javascript)
   * @return {null|object} - A set of errors by line number
   */
  _result (error, value, packed) {
    if (packed !== false) packed = true

    let errors = null
    if (error) {
      // Parse the error stack to get message and line number
      let lines = error.stack.split('\n')
      let match = lines[1].match(/<anonymous>:(\d+):(\d+)/)
      let line = 0
      let column = 0
      if (match) {
        line = parseInt(match[1], 10) - 1
        column = parseInt(match[2], 10)
      }
      let message = lines[0] || error.message

      errors = [{
        line: line,
        column: column,
        message: message
      }]
    }

    let output
    if (value === undefined) output = null
    else if (packed) output = pack(value)
    else output = value

    return {
      errors: errors,
      output: output
    }
  }

}
<|endoftext|>


<commit_before>(function () {
    // "use strict";
    var $win = $(window),
        $chart = $('#head-chart'),
        $btnZoom = $('#chart-zoomout'),
        baseConfig = {
            chart: {
                renderTo: 'head-chart',
                zoomType: 'x',
                events: {
                    selection: function() {
                        $btnZoom.removeClass('inactive')
                                .click(_pd(function(e) {
                                    $(this).trigger('zoomout');
                                }));
                    }
                }
            },
            credits: { enabled: false },
            title: {
                text: null
            },
            xAxis: {
                type: 'datetime',
                maxZoom: 7 * 24 * 3600000, // seven days
                title: {
                    text: null
                },
                tickmarkPlacement: 'on'
            },
            yAxis: {
                title: {
                    text: null
                },
                labels: {
                    formatter: function() {
                        return Highcharts.numberFormat(this.value, 0);
                    }
                },
                minPadding: 0.05,
                startOnTick: false,
                showFirstLabel: false
            },
            legend: {
                enabled: true
            },
            tooltip: { },
            plotOptions: {
                line: {
                    lineWidth: 1,
                    animation: false,
                    shadow: false,
                    marker: {
                        enabled: true,
                        radius: 0,
                        states: {
                           hover: {
                              enabled: true,
                              radius: 5
                           }
                        }
                    },
                    states: {
                        hover: {
                            lineWidth: 2
                        }
                    }
                }
            }
        };
    Highcharts.setOptions({ lang: { resetZoom: '' } });
    var chart;
    // which unit do we use for a given metric?
    var metricTypes = {
        "usage"              : "users",
        "apps"               : "users",
        "locales"            : "users",
        "os"                 : "users",
        "versions"           : "users",
        "statuses"           : "users",
        "users_created"      : "users",
        "installs"           : "users",
        "downloads"          : "downloads",
        "sources"            : "downloads",
        "contributions"      : "currency",
        "revenue"            : "currency",
        "reviews_created"    : "reviews",
        "addons_in_use"      : "addons",
        "addons_created"     : "addons",
        "addons_updated"     : "addons",
        "addons_downloaded"  : "addons",
        "collections_created": "collections",
        "subscribers"        : "collections",
        "ratings"            : "collections",
        "sales"              : "sales",
        "refunds"            : "refunds"
    };

    var acceptedGroups = {
        'day'   : true,
        'week'  : true,
        'month' : true
    };

    function showNoDataOverlay() {
        $chart.parent().addClass('nodata');
        $chart.parent().removeClass('loading');
        if (chart && chart.destroy) chart.destroy();
    }

    $win.bind("changeview", function() {
        $chart.parent().removeClass('nodata');
        $chart.addClass('loading');
    });

    $win.bind("dataready", function(e, obj) {
        var view    = obj.view,
            metric  = view.metric,
            group   = view.group,
            data    = obj.data,
            range   = normalizeRange(view.range),
            start   = range.start,
            end     = range.end,
            fields  = obj.fields ? obj.fields.slice(0,5) : ['count'],
            series  = {},
            events  = obj.events,
            chartRange = {},
            t, row, i, field, val,
            is_overview = metric == 'overview' || metric == 'app_overview';

        if (!(group in acceptedGroups)) {
            group = 'day';
        }

        if (obj.data.empty || !data.firstIndex) {
            showNoDataOverlay();
            $chart.removeClass('loading');
            return;
        }

        // Initialize the empty series object.
        _.each(fields, function(f) { series[f] = []; });

        // Transmute the data into something Highcharts understands.
        start = Date.iso(data.firstIndex);
        z.data = data;
        var step = '1 ' + group,
            point,
            dataSum = 0;

        forEachISODate({start: start, end: end}, '1 '+group, data, function(row, d) {
            for (i = 0; i < fields.length; i++) {
                field = fields[i];
                val = parseFloat(z.StatsManager.getField(row, field));
                if (val != val) val = null;
                series[field].push(val);
                if (val) dataSum += val;
            }
        }, this);

        // highCharts seems to dislike 0 and null data when determining a yAxis range
        if (dataSum === 0) {
            baseConfig.yAxis.max = 10;
        } else {
            baseConfig.yAxis.max = null;
        }

        // Populate the chart config object.
        var chartData = [], id;
        for (i = 0; i < fields.length; i++) {
            field = fields[i];
            id = field.split("|").slice(-1)[0];
            chartData.push({
                'type'  : 'line',
                'name'  : z.StatsManager.getPrettyName(view.metric, id),
                'id'    : id,
                'pointInterval' : 1000 * 3600 * 24,
                // compensate for timezone offsets from UTC.
                'pointStart' : start.getTime() - start.getTimezoneOffset() * 60000,
                'data'  : series[field],
                'visible' : !(metric == 'contributions' && id !='total')
            });
        }

        // Generate the tooltip function for this chart.
        // both x and y axis can be displayed differently.
        var tooltipFormatter = (function(){
            var xFormatter,
                yFormatter;
            function dayFormatter(d) { return Highcharts.dateFormat('%a, %b %e, %Y', new Date(d)); }
            function weekFormatter(d) { return "Week of " + Highcharts.dateFormat('%b %e, %Y', new Date(d)); }
            function monthFormatter(d) { return Highcharts.dateFormat('%B %Y', new Date(d)); }
            function downloadFormatter(n) { return gettext('{0} downloads', Highcharts.numberFormat(n, 0)); }
            function userFormatter(n) { return gettext('{0} users', Highcharts.numberFormat(n, 0)); }
            function addonsFormatter(n) { return gettext('{0} addons', Highcharts.numberFormat(n, 0)); }
            function collectionsFormatter(n) { return gettext('{0} collections', Highcharts.numberFormat(n, 0)); }
            function reviewsFormatter(n) { return gettext('{0} reviews', Highcharts.numberFormat(n, 0)); }
            function currencyFormatter(n) { return '$' + Highcharts.numberFormat(n, 2); }
            function salesFormatter(n) { return gettext('{0} sales', Highcharts.numberFormat(n, 0)); }
            function refundsFormatter(n) { return gettext('{0} refunds', Highcharts.numberFormat(n, 0)); }
            function addEventData(s, date) {
                var e = events[date];
                if (e) {
                    s += format('

{type_pretty}', e);
                }
                return s;
            }

            // Determine x-axis formatter.
            if (group == "week") {
                xFormatter = weekFormatter;
            } else if (group == "month") {
                xFormatter = monthFormatter;
            } else {
                xFormatter = dayFormatter;
            }

            if (is_overview) {
                return function() {
                    var ret = "" + xFormatter(this.x) + "",
                        p;
                    for (var i=0; i < this.points.length; i++) {
                        p = this.points[i];
                        ret += '
' + p.series.name + ': ';
                        ret += Highcharts.numberFormat(p.y, 0);
                    }
                    return addEventData(ret, this.x);
                };
            } else if (metric == 'contributions') {
                return function() {
                    var ret = "" + xFormatter(this.x) + "",
                        p;
                    for (var i=0; i < this.points.length; i++) {
                        p = this.points[i];
                        ret += '
' + p.series.name + ': ';
                        if (p.series.options.yAxis > 0) {
                            ret += Highcharts.numberFormat(p.y, 0);
                        } else {
                            ret += currencyFormatter(p.y);
                        }
                    }
                    return addEventData(ret, this.x);
                };
            } else {
                // Determine y-axis formatter.
                switch (metricTypes[metric]) {
                    case "users":
                        yFormatter = userFormatter;
                        break;
                    case "downloads":
                        yFormatter = downloadFormatter;
                        break;
                    case "currency": case "revenue":
                        yFormatter = currencyFormatter;
                        break;
                    case "collections":
                        yFormatter = collectionsFormatter;
                        break;
                    case "reviews":
                        yFormatter = reviewsFormatter;
                        break;
                    case "addons":
                        yFormatter = addonsFormatter;
                        break;
                        yFormatter = currencyFormatter;
                        break;
                    case "sales":
                        yFormatter = salesFormatter;
                        break;
                    case "refunds":
                        yFormatter = refundsFormatter;
                        break;
                }
                return function() {
                    var ret = "" + this.series.name + "
" +
                              xFormatter(this.x) + "
" +
                              yFormatter(this.y);
                    return addEventData(ret, this.x);
                };
            }
        })();

        // Set up the new chart's configuration.
        var newConfig = $.extend(baseConfig, { series: chartData });
        // set up dual-axes for the overview chart.
        if (is_overview && newConfig.series.length) {
            _.extend(newConfig, {
                yAxis : [
                    { // Downloads
                        title: {
                           text: gettext('Downloads')
                        },
                        min: 0,
                        labels: {
                            formatter: function() {
                                return Highcharts.numberFormat(this.value, 0);
                            }
                        }
                    }, { // Daily Users
                        title: {
                            text: gettext('Daily Users')
                        },
                        labels: {
                            formatter: function() {
                                return Highcharts.numberFormat(this.value, 0);
                            }
                        },
                        min: 0,
                        opposite: true
                    }
                ],
                tooltip: {
                    shared : true,
                    crosshairs : true
                }
            });
            // set Daily Users series to use the right yAxis.
            if (metric == 'overview') {
                _.find(newConfig.series,
                   function(s) { return s.id == 'updates'; }).yAxis = 1;
            } else {
                _.find(newConfig.series,
                   function(s) { return s.id == 'usage'; }).yAxis = 1;
            }

        }
        if (metric == "contributions" && newConfig.series.length) {
            _.extend(newConfig, {
                yAxis : [
                    { // Amount
                        title: {
                            text: gettext('Amount, in USD')
                        },
                        labels: {
                            formatter: function() {
                                return Highcharts.numberFormat(this.value, 2);
                            }
                        },
                        min: 0
                    },
                    { // Number of Contributions
                        title: {
                           text: gettext('Number of Contributions')
                        },
                        min: 0,
                        labels: {
                            formatter: function() {
                                return Highcharts.numberFormat(this.value, 0);
                            }
                        },
                        opposite: true
                    }
                ],
                tooltip: {
                    shared : true,
                    crosshairs : true
                }
            });
            // set Daily Users series to use the right yAxis.
            newConfig.series[0].yAxis = 1;
        }
        newConfig.tooltip.formatter = tooltipFormatter;

        function makeSiteEventHandler(e) {
            return function() {
                var s = format('
{type_pretty}

{description}
', e);
                if (e.url) {
                    s += format('
{1}
', [e.url, gettext('More Info...')]);
                }
                $('#exception-note h2').html(format(
                    // L10n: {0} is an ISO-formatted date.
                    gettext('Details for {0}'),
                    e.start
                ));
                $('#exception-note div').html(s);
                $chart.trigger('explain-exception');
            };
        }

        var pb = [], pl = [];
        eventColors = ['#DDD','#DDD','#FDFFD0','#D0FFD8'];
        _.forEach(events, function(e) {
            pb.push({
                color: eventColors[e.type],
                from: Date.iso(e.start).backward('12h'),
                to: Date.iso(e.end || e.start).forward('12h'),
                events: {
                    click: makeSiteEventHandler(e)
                }
            });
        });
        newConfig.xAxis.plotBands = pb;
        newConfig.xAxis.plotLines = pl;

        if (fields.length == 1) {
            newConfig.legend.enabled = false;
        }

        // Generate a pretty title for the chart.
        var title;
        if (typeof obj.view.range == 'string') {
            var numDays = parseInt(obj.view.range, 10);
            title = format(csv_keys.chartTitle[metric][0], numDays);
        } else {
            // This is a custom range so display a range shorter by one day.
            end = new Date(end.getTime() - (24 * 60 * 60 * 1000));
            title = format(csv_keys.chartTitle[metric][1], [start.iso(), end.iso()]);
        }
        newConfig.title = {
            text: title
        };
        if (chart && chart.destroy) chart.destroy();
        chart = new Highcharts.Chart(newConfig);

        chartRange = chart.xAxis[0].getExtremes();

        $win.bind('zoomout', function() {
            chart.xAxis[0].setExtremes(chartRange.min, chartRange.max);
            $btnZoom.addClass('inactive').click(_pd);
        });

        $chart.removeClass('loading');
    });
})();
<commit_msg>fix l10n formatting in chart js (bug 755536)<commit_after>(function () {
    // "use strict";
    var $win = $(window),
        $chart = $('#head-chart'),
        $btnZoom = $('#chart-zoomout'),
        baseConfig = {
            chart: {
                renderTo: 'head-chart',
                zoomType: 'x',
                events: {
                    selection: function() {
                        $btnZoom.removeClass('inactive')
                                .click(_pd(function(e) {
                                    $(this).trigger('zoomout');
                                }));
                    }
                }
            },
            credits: { enabled: false },
            title: {
                text: null
            },
            xAxis: {
                type: 'datetime',
                maxZoom: 7 * 24 * 3600000, // seven days
                title: {
                    text: null
                },
                tickmarkPlacement: 'on'
            },
            yAxis: {
                title: {
                    text: null
                },
                labels: {
                    formatter: function() {
                        return Highcharts.numberFormat(this.value, 0);
                    }
                },
                minPadding: 0.05,
                startOnTick: false,
                showFirstLabel: false
            },
            legend: {
                enabled: true
            },
            tooltip: { },
            plotOptions: {
                line: {
                    lineWidth: 1,
                    animation: false,
                    shadow: false,
                    marker: {
                        enabled: true,
                        radius: 0,
                        states: {
                           hover: {
                              enabled: true,
                              radius: 5
                           }
                        }
                    },
                    states: {
                        hover: {
                            lineWidth: 2
                        }
                    }
                }
            }
        };
    Highcharts.setOptions({ lang: { resetZoom: '' } });
    var chart;
    // which unit do we use for a given metric?
    var metricTypes = {
        "usage"              : "users",
        "apps"               : "users",
        "locales"            : "users",
        "os"                 : "users",
        "versions"           : "users",
        "statuses"           : "users",
        "users_created"      : "users",
        "installs"           : "users",
        "downloads"          : "downloads",
        "sources"            : "downloads",
        "contributions"      : "currency",
        "revenue"            : "currency",
        "reviews_created"    : "reviews",
        "addons_in_use"      : "addons",
        "addons_created"     : "addons",
        "addons_updated"     : "addons",
        "addons_downloaded"  : "addons",
        "collections_created": "collections",
        "subscribers"        : "collections",
        "ratings"            : "collections",
        "sales"              : "sales",
        "refunds"            : "refunds"
    };

    var acceptedGroups = {
        'day'   : true,
        'week'  : true,
        'month' : true
    };

    function showNoDataOverlay() {
        $chart.parent().addClass('nodata');
        $chart.parent().removeClass('loading');
        if (chart && chart.destroy) chart.destroy();
    }

    $win.bind("changeview", function() {
        $chart.parent().removeClass('nodata');
        $chart.addClass('loading');
    });

    $win.bind("dataready", function(e, obj) {
        var view    = obj.view,
            metric  = view.metric,
            group   = view.group,
            data    = obj.data,
            range   = normalizeRange(view.range),
            start   = range.start,
            end     = range.end,
            fields  = obj.fields ? obj.fields.slice(0,5) : ['count'],
            series  = {},
            events  = obj.events,
            chartRange = {},
            t, row, i, field, val,
            is_overview = metric == 'overview' || metric == 'app_overview';

        if (!(group in acceptedGroups)) {
            group = 'day';
        }

        if (obj.data.empty || !data.firstIndex) {
            showNoDataOverlay();
            $chart.removeClass('loading');
            return;
        }

        // Initialize the empty series object.
        _.each(fields, function(f) { series[f] = []; });

        // Transmute the data into something Highcharts understands.
        start = Date.iso(data.firstIndex);
        z.data = data;
        var step = '1 ' + group,
            point,
            dataSum = 0;

        forEachISODate({start: start, end: end}, '1 '+group, data, function(row, d) {
            for (i = 0; i < fields.length; i++) {
                field = fields[i];
                val = parseFloat(z.StatsManager.getField(row, field));
                if (val != val) val = null;
                series[field].push(val);
                if (val) dataSum += val;
            }
        }, this);

        // highCharts seems to dislike 0 and null data when determining a yAxis range
        if (dataSum === 0) {
            baseConfig.yAxis.max = 10;
        } else {
            baseConfig.yAxis.max = null;
        }

        // Populate the chart config object.
        var chartData = [], id;
        for (i = 0; i < fields.length; i++) {
            field = fields[i];
            id = field.split("|").slice(-1)[0];
            chartData.push({
                'type'  : 'line',
                'name'  : z.StatsManager.getPrettyName(view.metric, id),
                'id'    : id,
                'pointInterval' : 1000 * 3600 * 24,
                // compensate for timezone offsets from UTC.
                'pointStart' : start.getTime() - start.getTimezoneOffset() * 60000,
                'data'  : series[field],
                'visible' : !(metric == 'contributions' && id !='total')
            });
        }

        // Generate the tooltip function for this chart.
        // both x and y axis can be displayed differently.
        var tooltipFormatter = (function(){
            var xFormatter,
                yFormatter;
            function dayFormatter(d) { return Highcharts.dateFormat('%a, %b %e, %Y', new Date(d)); }
            function weekFormatter(d) { return format(gettext('Week of {0}'), Highcharts.dateFormat('%b %e, %Y', new Date(d))); }
            function monthFormatter(d) { return Highcharts.dateFormat('%B %Y', new Date(d)); }
            function downloadFormatter(n) { return gettext(Highcharts.numberFormat(n, 0) + 'downloads'); }
            function userFormatter(n) { return format(gettext('{0} users'), Highcharts.numberFormat(n, 0)); }
            function addonsFormatter(n) { return format(gettext('{0} addons'), Highcharts.numberFormat(n, 0)); }
            function collectionsFormatter(n) { return format(gettext('{0} collections'), Highcharts.numberFormat(n, 0)); }
            function reviewsFormatter(n) { return format(gettext('{0} reviews'), Highcharts.numberFormat(n, 0)); }
            function currencyFormatter(n) { return '$' + Highcharts.numberFormat(n, 2); }
            function salesFormatter(n) { return format(gettext('{0} sales'), Highcharts.numberFormat(n, 0)); }
            function refundsFormatter(n) { return format(gettext('{0} refunds'), Highcharts.numberFormat(n, 0)); }
            function addEventData(s, date) {
                var e = events[date];
                if (e) {
                    s += format('

{type_pretty}', e);
                }
                return s;
            }

            // Determine x-axis formatter.
            if (group == "week") {
                xFormatter = weekFormatter;
            } else if (group == "month") {
                xFormatter = monthFormatter;
            } else {
                xFormatter = dayFormatter;
            }

            if (is_overview) {
                return function() {
                    var ret = "" + xFormatter(this.x) + "",
                        p;
                    for (var i=0; i < this.points.length; i++) {
                        p = this.points[i];
                        ret += '
' + p.series.name + ': ';
                        ret += Highcharts.numberFormat(p.y, 0);
                    }
                    return addEventData(ret, this.x);
                };
            } else if (metric == 'contributions') {
                return function() {
                    var ret = "" + xFormatter(this.x) + "",
                        p;
                    for (var i=0; i < this.points.length; i++) {
                        p = this.points[i];
                        ret += '
' + p.series.name + ': ';
                        if (p.series.options.yAxis > 0) {
                            ret += Highcharts.numberFormat(p.y, 0);
                        } else {
                            ret += currencyFormatter(p.y);
                        }
                    }
                    return addEventData(ret, this.x);
                };
            } else {
                // Determine y-axis formatter.
                switch (metricTypes[metric]) {
                    case "users":
                        yFormatter = userFormatter;
                        break;
                    case "downloads":
                        yFormatter = downloadFormatter;
                        break;
                    case "currency": case "revenue":
                        yFormatter = currencyFormatter;
                        break;
                    case "collections":
                        yFormatter = collectionsFormatter;
                        break;
                    case "reviews":
                        yFormatter = reviewsFormatter;
                        break;
                    case "addons":
                        yFormatter = addonsFormatter;
                        break;
                    case "sales":
                        yFormatter = salesFormatter;
                        break;
                    case "refunds":
                        yFormatter = refundsFormatter;
                        break;
                }
                return function() {
                    var ret = "" + this.series.name + "
" +
                              xFormatter(this.x) + "
" +
                              yFormatter(this.y);
                    return addEventData(ret, this.x);
                };
            }
        })();

        // Set up the new chart's configuration.
        var newConfig = $.extend(baseConfig, { series: chartData });
        // set up dual-axes for the overview chart.
        if (is_overview && newConfig.series.length) {
            _.extend(newConfig, {
                yAxis : [
                    { // Downloads
                        title: {
                           text: gettext('Downloads')
                        },
                        min: 0,
                        labels: {
                            formatter: function() {
                                return Highcharts.numberFormat(this.value, 0);
                            }
                        }
                    }, { // Daily Users
                        title: {
                            text: gettext('Daily Users')
                        },
                        labels: {
                            formatter: function() {
                                return Highcharts.numberFormat(this.value, 0);
                            }
                        },
                        min: 0,
                        opposite: true
                    }
                ],
                tooltip: {
                    shared : true,
                    crosshairs : true
                }
            });
            // set Daily Users series to use the right yAxis.
            if (metric == 'overview') {
                _.find(newConfig.series,
                   function(s) { return s.id == 'updates'; }).yAxis = 1;
            } else {
                _.find(newConfig.series,
                   function(s) { return s.id == 'usage'; }).yAxis = 1;
            }

        }
        if (metric == "contributions" && newConfig.series.length) {
            _.extend(newConfig, {
                yAxis : [
                    { // Amount
                        title: {
                            text: gettext('Amount, in USD')
                        },
                        labels: {
                            formatter: function() {
                                return Highcharts.numberFormat(this.value, 2);
                            }
                        },
                        min: 0
                    },
                    { // Number of Contributions
                        title: {
                           text: gettext('Number of Contributions')
                        },
                        min: 0,
                        labels: {
                            formatter: function() {
                                return Highcharts.numberFormat(this.value, 0);
                            }
                        },
                        opposite: true
                    }
                ],
                tooltip: {
                    shared : true,
                    crosshairs : true
                }
            });
            // set Daily Users series to use the right yAxis.
            newConfig.series[0].yAxis = 1;
        }
        newConfig.tooltip.formatter = tooltipFormatter;

        function makeSiteEventHandler(e) {
            return function() {
                var s = format('
{type_pretty}

{description}
', e);
                if (e.url) {
                    s += format('
{1}
', [e.url, gettext('More Info...')]);
                }
                $('#exception-note h2').html(format(
                    // L10n: {0} is an ISO-formatted date.
                    gettext('Details for {0}'),
                    e.start
                ));
                $('#exception-note div').html(s);
                $chart.trigger('explain-exception');
            };
        }

        var pb = [], pl = [];
        eventColors = ['#DDD','#DDD','#FDFFD0','#D0FFD8'];
        _.forEach(events, function(e) {
            pb.push({
                color: eventColors[e.type],
                from: Date.iso(e.start).backward('12h'),
                to: Date.iso(e.end || e.start).forward('12h'),
                events: {
                    click: makeSiteEventHandler(e)
                }
            });
        });
        newConfig.xAxis.plotBands = pb;
        newConfig.xAxis.plotLines = pl;

        if (fields.length == 1) {
            newConfig.legend.enabled = false;
        }

        // Generate a pretty title for the chart.
        var title;
        if (typeof obj.view.range == 'string') {
            var numDays = parseInt(obj.view.range, 10);
            title = format(csv_keys.chartTitle[metric][0], numDays);
        } else {
            // This is a custom range so display a range shorter by one day.
            end = new Date(end.getTime() - (24 * 60 * 60 * 1000));
            title = format(csv_keys.chartTitle[metric][1], [start.iso(), end.iso()]);
        }
        newConfig.title = {
            text: title
        };
        if (chart && chart.destroy) chart.destroy();
        chart = new Highcharts.Chart(newConfig);

        chartRange = chart.xAxis[0].getExtremes();

        $win.bind('zoomout', function() {
            chart.xAxis[0].setExtremes(chartRange.min, chartRange.max);
            $btnZoom.addClass('inactive').click(_pd);
        });

        $chart.removeClass('loading');
    });
})();
<|endoftext|>