<commit_before>/* Модуль сжатия js-скриптов и css*/

/* функция сжимает js-скрипты
 * и сохраняет их с именем .min.js
 */

var fs = require('fs');
var path=require('path');

/* CONSTANTS */
/* dir contains css-files */
var CSSDIR      = 'css/';

/* ---------------------------------- */
console.log('minify.js loaded...');
console.log('current dir: ' + process.cwd());
/* ---------------------------------- */

exports.jsScripts=function jsScripts(){
    'use strict';

    /* подключаем модуль uglify-js
     * если его нет - дальнейшая
     * работа функции не имеет смысла
     */
    try{
        var jsp = require("uglify-js").parser;
        var pro = require("uglify-js").uglify;
    }catch(error){
        console.log('can\'n load uglify-js\n'                  +
            'to use js-minification you need to install uglify-js\n'    +
                'npm install uglify-js\n'                               +
                'https://github.com/mishoo/UglifyJS');
        return false;
    }
    /* Константы */

    var CLIENT_JS='client.js';
    var CLOUDFUNC_JS='lib/cloudfunc.js';
    var CLIENT_KEYBINDING_JS='lib/client/keyBinding.js';

    var dataReaded_f=function(pFileName, pData){
            console.log('file ' + pFileName + ' readed');

            /*********************************/
            /* сжимаем код через uglify-js */
            var uglify_js=function(pDdata){
                var orig_code = pDdata.toString();
                var ast = jsp.parse(orig_code); // parse code and get the initial AST
                ast = pro.ast_mangle(ast); // get a new AST with mangled names
                ast = pro.ast_squeeze(ast); // get an AST with compression optimizations
                var result_code = pro.gen_code(ast); // compressed code here
                return result_code;
            };
            /*********************************/
            var final_code=uglify_js(pData);

            var minFileName=pFileName.replace('.js','.min.js');
            /* если мы сжимаем client.js -
             * меняем строку cloudfunc.js на
             * cloudfunc.min.js и выводим сообщение
             *
             * меняем строку keyBinding.js на
             * keyBinding.min.js
             * если другой файл - ничего не деалем
             */
            if(pFileName===CLIENT_JS)
                console.log('file name of ' +
                    CLOUDFUNC_JS            +
                    ' in '                  +
                    CLIENT_JS               +
                    ' changed. size:',
                    (final_code=final_code
                        .replace('cloudfunc.js','cloudfunc.min.js')
                            .replace('keyBinding.js','keyBinding.min.js')).length);

            /* записываем сжатый js-скрипт*/
            fs.writeFile(path.basename(minFileName), final_code, fileWrited(minFileName));
        };
     console.log('reading file ' + CLIENT_JS+'...');
    fs.readFile(CLIENT_JS,fileReaded(CLIENT_JS,dataReaded_f));

    console.log('reading file ' + CLOUDFUNC_JS+'...');
    fs.readFile(CLOUDFUNC_JS,fileReaded(CLOUDFUNC_JS,dataReaded_f));

    console.log('reading file ' + CLIENT_KEYBINDING_JS+'...');
    fs.readFile(CLIENT_KEYBINDING_JS, fileReaded(CLIENT_KEYBINDING_JS,dataReaded_f));

    return true;
};

/* функция сжимает css-стили
 * и сохраняет их с именем .min.css
 * @pImgConvertToBase64_b - булевый признак,
 * который отвечает за то, что быконвертировать
 * картинки в base64 и поместить в выходной css файл
 */
exports.cssStyles=function cssStyles(pImgConvertToBase64_b){
    'use strict';

     /* connecting cleanCSS,
      * if we can't find it -
      * return false
      */
     var cleanCSS;
     try{
        cleanCSS = require('clean-css');
    }catch(error){
        console.log('can\'n load clean-css \n'                          +
            'to use css-minification you need to install clean-css \n'  +
                'npm install clean-css\n'                               +
                'https://github.com/GoalSmashers/clean-css');
        return false;
    }

    /* Константы */
    var STYLE_CSS   = CSSDIR+'style.css';
    var RESET_CSS   = CSSDIR+'reset.css';

    var lAllStyle='';
    var lResetCssDone=false;
    var lStyleCssDone=false;
    var dataReaded_f=function(pFileName, pData){
        console.log('file ' + pFileName + ' readed');
        /*********************************/
        /* сжимаем код через clean-css */
        var clean_css=function(pData){
            /* Сохраняем весь стиль в одну переменную*/
            return cleanCSS.process(pData);
        };
        /*********************************/
        var final_code=clean_css(pData);

        lAllStyle+=final_code;

        var minFileName=pFileName.replace('.css','.min.css');

        if(pFileName===STYLE_CSS)lStyleCssDone=true;
        if(pFileName===RESET_CSS)lResetCssDone=true;
        /* if all files writed we
         * save all minimized css
         * to one file all.min.css
         */
        if(lStyleCssDone && lResetCssDone){
            /* если включена конвертация картинок в base64
             * вызываем её
             */
            if(pImgConvertToBase64_b)
                base64_images(lAllStyle);
            else
                fs.writeFile('all.min.css', lAllStyle, fileWrited('all.min.css'));
        }
         /* в другом случае - записываем сжатый css файл*/
        else fs.writeFile(path.basename(minFileName), final_code, fileWrited(minFileName));
    };

    console.log('reading file ' + STYLE_CSS+'...');
    fs.readFile(STYLE_CSS,fileReaded(STYLE_CSS,dataReaded_f));

    console.log('reading file ' + RESET_CSS+'...');
    fs.readFile(RESET_CSS,fileReaded(RESET_CSS,dataReaded_f));

    return true;
};

/* функция сжимает css-стили
 * и сохраняет их с именем .min.css
 */
exports.html=function(){
    'use strict';

     /* connecting cleanCSS,
      * if we can't find it -
      * return false
      */
     var htmlMinifier;
     try{
        htmlMinifier = require('html-minifier');
    }catch(error){
        console.log('can\'n load html-minifier \n'                 +
            'to use html-minification you need to install html-minifier\n'  +
                'npm install html-minifier\n'                               +
                'https://github.com/kangax/html-minifier');
        return false;
    }

    /* Константы */
    var INDEX_HTML='index.html';

    var dataReaded_f=function(pFileName, pData){
        console.log('file ' + pFileName + ' readed');
        /*********************************/
        /* сжимаем код через clean-css */
        var html_minify=function(pData){
            /* Сохраняем весь стиль в одну переменную*/

            var lOptions={
                removeComments:                 true,
                removeCommentsFromCDATA:        true,
                removeCDATASectionsFromCDATA:   true,
                collapseWhitespace:             true,
                collapseBooleanAttributes:      true,
                removeAttributeQuotes:          true,
                removeRedundantAttributes:      true,
                useShortDoctype:                true,
                removeEmptyAttributes:          true,
                /* оставляем, поскольку у нас
                 * в элемент fm генерируеться
                 * таблица файлов
                 */
                removeEmptyElements:            false,
                removeOptionalTags:             true,
                removeScriptTypeAttributes:     true,
                removeStyleLinkTypeAttributes:  true
            };

            return htmlMinifier.minify(pData,lOptions);
        };
        /*********************************/
        var final_code=html_minify(pData);

        var minFileName=pFileName.replace('.html','.min.html');

         /* записываем сжатый html файл*/
        fs.writeFile(minFileName, final_code, fileWrited(minFileName));
    };

    console.log('reading file ' + INDEX_HTML+'...');
    fs.readFile(INDEX_HTML,fileReaded(INDEX_HTML,dataReaded_f));

    return true;
};

/* функция переводит картинки в base64 и записывает в css-файл*/
function base64_images(pFileContent_s){
    'use strict';
     var b64img;
     try{
        b64img = require('css-b64-images');
    }catch(error){
        console.log('can\'n load clean-css \n'                 +
            'to use images to base64 convertation you need to install css-base64-images \n'  +
                'npm install -g css-b64-images\n'                               +
                'https://github.com/Filirom1/css-base64-images');
        return false;
    }
    b64img.fromString(pFileContent_s, '.','', function(err, css){
        console.log('images converted to base64 and saved in css file');
        fs.writeFile('all.min.css', css, fileWrited('all.min.css'));
    });
}

/* Функция создаёт асинхроную версию
 * для чтения файла
 * @pFileName - имя считываемого файла
 */
function fileReaded(pFileName,pCompressFunc){
    "use strict";
    return function(pError,pData){
        /* функция в которую мы попадаем,
         * если данные считались
         *
         * если ошибка - показываем её
         * иначе если переданная функция -
         * функция запускаем её
         */
        if(!pError)
            if (pCompressFunc && typeof pCompressFunc==="function")
                    pCompressFunc(pFileName,pData.toString());
        else console.log(pError);
    };
}

/*
 * Функция вызываеться после записи файла
 * и выводит ошибку или сообщает,
 * что файл успешно записан
 */
function fileWrited(pFileName){
    "use strict";
    return function(error){
        console.log(error?error:('file '+pFileName+' writed...'));
    };
}<commit_msg>nodester<commit_after>/* Модуль сжатия js-скриптов и css*/

/* функция сжимает js-скрипты
 * и сохраняет их с именем .min.js
 */

var fs = require('fs');
var path=require('path');

/* CONSTANTS */
/* dir contains css-files */
var CSSDIR      = 'css/';

/* ---------------------------------- */
console.log('minify.js loaded...');
console.log('current dir: ' + process.cwd());
/* ---------------------------------- */

exports.jsScripts=function jsScripts(){
    'use strict';

    /* подключаем модуль uglify-js
     * если его нет - дальнейшая
     * работа функции не имеет смысла
     */
    try{
        var jsp = require("uglify-js").parser;
        var pro = require("uglify-js").uglify;
    }catch(error){
        console.log('can\'n load uglify-js\n'                  +
            'to use js-minification you need to install uglify-js\n'    +
                'npm install uglify-js\n'                               +
                'https://github.com/mishoo/UglifyJS');
        return false;
    }
    /* Константы */
    var CLIENT_JS='client.js';
    var CLOUDFUNC_JS='lib/cloudfunc.js';
    var CLIENT_KEYBINDING_JS='lib/client/keyBinding.js';

    var dataReaded_f=function(pFileName, pData){
            console.log('file ' + pFileName + ' readed');

            /*********************************/
            /* сжимаем код через uglify-js */
            var uglify_js=function(pDdata){
                var orig_code = pDdata.toString();
                var ast = jsp.parse(orig_code); // parse code and get the initial AST
                ast = pro.ast_mangle(ast); // get a new AST with mangled names
                ast = pro.ast_squeeze(ast); // get an AST with compression optimizations
                var result_code = pro.gen_code(ast); // compressed code here
                return result_code;
            };
            /*********************************/
            var final_code=uglify_js(pData);

            var minFileName=pFileName.replace('.js','.min.js');
            /* если мы сжимаем client.js -
             * меняем строку cloudfunc.js на
             * cloudfunc.min.js и выводим сообщение
             *
             * меняем строку keyBinding.js на
             * keyBinding.min.js
             * если другой файл - ничего не деалем
             */
            if(pFileName===CLIENT_JS)
                console.log('file name of ' +
                    CLOUDFUNC_JS            +
                    ' in '                  +
                    CLIENT_JS               +
                    ' changed. size:',
                    (final_code=final_code
                        .replace('cloudfunc.js','cloudfunc.min.js')
                            .replace('keyBinding.js','keyBinding.min.js')).length);

            /* записываем сжатый js-скрипт*/
            fs.writeFile(path.basename(minFileName), final_code, fileWrited(minFileName));
        };
     console.log('reading file ' + CLIENT_JS+'...');
    fs.readFile(CLIENT_JS,fileReaded(CLIENT_JS,dataReaded_f));

    console.log('reading file ' + CLOUDFUNC_JS+'...');
    fs.readFile(CLOUDFUNC_JS,fileReaded(CLOUDFUNC_JS,dataReaded_f));

    console.log('reading file ' + CLIENT_KEYBINDING_JS+'...');
    fs.readFile(CLIENT_KEYBINDING_JS, fileReaded(CLIENT_KEYBINDING_JS,dataReaded_f));

    return true;
};

/* функция сжимает css-стили
 * и сохраняет их с именем .min.css
 * @pImgConvertToBase64_b - булевый признак,
 * который отвечает за то, что быконвертировать
 * картинки в base64 и поместить в выходной css файл
 */
exports.cssStyles=function cssStyles(pImgConvertToBase64_b){
    'use strict';

     /* connecting cleanCSS,
      * if we can't find it -
      * return false
      */
     var cleanCSS;
     try{
        cleanCSS = require('clean-css');
    }catch(error){
        console.log('can\'n load clean-css \n'                          +
            'to use css-minification you need to install clean-css \n'  +
                'npm install clean-css\n'                               +
                'https://github.com/GoalSmashers/clean-css');
        return false;
    }

    /* Константы */
    var STYLE_CSS   = CSSDIR+'style.css';
    var RESET_CSS   = CSSDIR+'reset.css';

    var lAllStyle='';
    var lResetCssDone=false;
    var lStyleCssDone=false;
    var dataReaded_f=function(pFileName, pData){
        console.log('file ' + pFileName + ' readed');
        /*********************************/
        /* сжимаем код через clean-css */
        var clean_css=function(pData){
            /* Сохраняем весь стиль в одну переменную*/
            return cleanCSS.process(pData);
        };
        /*********************************/
        var final_code=clean_css(pData);

        lAllStyle+=final_code;

        var minFileName=pFileName.replace('.css','.min.css');

        if(pFileName===STYLE_CSS)lStyleCssDone=true;
        if(pFileName===RESET_CSS)lResetCssDone=true;
        /* if all files writed we
         * save all minimized css
         * to one file all.min.css
         */
        if(lStyleCssDone && lResetCssDone){
            /* если включена конвертация картинок в base64
             * вызываем её
             */
            if(pImgConvertToBase64_b)
                base64_images(lAllStyle);
            else
                fs.writeFile('all.min.css', lAllStyle, fileWrited('all.min.css'));
        }
         /* в другом случае - записываем сжатый css файл*/
        else fs.writeFile(path.basename(minFileName), final_code, fileWrited(minFileName));
    };

    console.log('reading file ' + STYLE_CSS+'...');
    fs.readFile(STYLE_CSS,fileReaded(STYLE_CSS,dataReaded_f));

    console.log('reading file ' + RESET_CSS+'...');
    fs.readFile(RESET_CSS,fileReaded(RESET_CSS,dataReaded_f));

    return true;
};

/* функция сжимает css-стили
 * и сохраняет их с именем .min.css
 */
exports.html=function(){
    'use strict';

     /* connecting cleanCSS,
      * if we can't find it -
      * return false
      */
     var htmlMinifier;
     try{
        htmlMinifier = require('html-minifier');
    }catch(error){
        console.log('can\'n load html-minifier \n'                 +
            'to use html-minification you need to install html-minifier\n'  +
                'npm install html-minifier\n'                               +
                'https://github.com/kangax/html-minifier');
        return false;
    }

    /* Константы */
    var INDEX_HTML='index.html';

    var dataReaded_f=function(pFileName, pData){
        console.log('file ' + pFileName + ' readed');
        /*********************************/
        /* сжимаем код через clean-css */
        var html_minify=function(pData){
            /* Сохраняем весь стиль в одну переменную*/

            var lOptions={
                removeComments:                 true,
                removeCommentsFromCDATA:        true,
                removeCDATASectionsFromCDATA:   true,
                collapseWhitespace:             true,
                collapseBooleanAttributes:      true,
                removeAttributeQuotes:          true,
                removeRedundantAttributes:      true,
                useShortDoctype:                true,
                removeEmptyAttributes:          true,
                /* оставляем, поскольку у нас
                 * в элемент fm генерируеться
                 * таблица файлов
                 */
                removeEmptyElements:            false,
                removeOptionalTags:             true,
                removeScriptTypeAttributes:     true,
                removeStyleLinkTypeAttributes:  true
            };

            return htmlMinifier.minify(pData,lOptions);
        };
        /*********************************/
        var final_code=html_minify(pData);

        var minFileName=pFileName.replace('.html','.min.html');

         /* записываем сжатый html файл*/
        fs.writeFile(minFileName, final_code, fileWrited(minFileName));
    };

    console.log('reading file ' + INDEX_HTML+'...');
    fs.readFile(INDEX_HTML,fileReaded(INDEX_HTML,dataReaded_f));

    return true;
};

/* функция переводит картинки в base64 и записывает в css-файл*/
function base64_images(pFileContent_s){
    'use strict';
     var b64img;
     try{
        b64img = require('css-b64-images');
    }catch(error){
        console.log('can\'n load clean-css \n'                 +
            'to use images to base64 convertation you need to install css-base64-images \n'  +
                'npm install -g css-b64-images\n'                               +
                'https://github.com/Filirom1/css-base64-images');
        return false;
    }
    b64img.fromString(pFileContent_s, '.','', function(err, css){
        console.log('images converted to base64 and saved in css file');
        fs.writeFile('all.min.css', css, fileWrited('all.min.css'));
    });
}

/* Функция создаёт асинхроную версию
 * для чтения файла
 * @pFileName - имя считываемого файла
 */
function fileReaded(pFileName,pCompressFunc){
    "use strict";
    return function(pError,pData){
        /* функция в которую мы попадаем,
         * если данные считались
         *
         * если ошибка - показываем её
         * иначе если переданная функция -
         * функция запускаем её
         */
        if(!pError)
            if (pCompressFunc && typeof pCompressFunc==="function")
                    pCompressFunc(pFileName,pData.toString());
        else console.log(pError);
    };
}

/*
 * Функция вызываеться после записи файла
 * и выводит ошибку или сообщает,
 * что файл успешно записан
 */
function fileWrited(pFileName){
    "use strict";
    return function(error){
        console.log(error?error:('file '+pFileName+' writed...'));
    };
}<|endoftext|>


<commit_before>/** @babel */
/** @jsx etch.dom */

import path from 'path'
import etch from 'etch'
import _ from 'underscore-plus'
import {CompositeDisposable, Disposable} from 'atom'

import GeneralPanel from './general-panel'
import EditorPanel from './editor-panel'
import PackageDetailView from './package-detail-view'
import KeybindingsPanel from './keybindings-panel'
import InstallPanel from './install-panel'
import ThemesPanel from './themes-panel'
import InstalledPackagesPanel from './installed-packages-panel'
import UpdatesPanel from './updates-panel'
import UriHandlerPanel from './uri-handler-panel'
import PackageManager from './package-manager'

export default class SettingsView {
  constructor ({uri, packageManager, snippetsProvider, activePanel} = {}) {
    this.uri = uri
    this.packageManager = packageManager
    this.snippetsProvider = snippetsProvider
    this.deferredPanel = activePanel
    this.destroyed = false

    etch.initialize(this)
    this.disposables = new CompositeDisposable()
    this.disposables.add(atom.commands.add(this.element, {
      'core:move-up': () => { this.scrollUp() },
      'core:move-down': () => { this.scrollDown() },
      'core:page-up': () => { this.pageUp() },
      'core:page-down': () => { this.pageDown() },
      'core:move-to-top': () => { this.scrollToTop() },
      'core:move-to-bottom': () => { this.scrollToBottom() }
    }))
    process.nextTick(() => this.initializePanels())
  }

  update () {}

  destroy () {
    this.destroyed = true
    this.disposables.dispose()
    for (let name in this.panelsByName) {
      const panel = this.panelsByName[name]
      panel.destroy()
    }

    return etch.destroy(this)
  }

  render () {
    return (

            <button className='btn btn-default icon icon-link-external' ref='openDotAtom'>Open Config Folder</button>

        {/* The tabindex attr below ensures that clicks in a panel item won't
        cause this view to gain focus. This is important because when this view
        gains focus (e.g. immediately after atom displays it), it focuses the
        currently active panel item. If that focusing causes the active panel to
        scroll (e.g. because the active panel itself passes focus on to a search
        box at the top of a scrolled panel), then the browser will not fire the
        click event on the element within the panel on which the user originally
        clicked (e.g. a package card). This would prevent us from showing a
        package detail view when clicking on a package card. Phew! */}

    )
  }

  // This prevents the view being actually disposed when closed
  // If you remove it you will need to ensure the cached settingsView
  // in main.coffee is correctly released on close as well...
  onDidChangeTitle () { return new Disposable() }

  initializePanels () {
    if (this.refs.panels.children.length > 1) {
      return
    }

    this.panelsByName = {}
    const clickHandler = (event) => {
      const target = event.target.closest('.panels-menu li a, .panels-packages li a')
      if (target) {
        this.showPanel(target.closest('li').name)
      }
    }
    this.element.addEventListener('click', clickHandler)
    this.disposables.add(new Disposable(() => this.element.removeEventListener('click', clickHandler)))

    const focusHandler = () => {
      this.focusActivePanel()
    }
    this.element.addEventListener('focus', focusHandler)
    this.disposables.add(new Disposable(() => this.element.removeEventListener('focus', focusHandler)))

    const openDotAtomClickHandler = () => {
      atom.open({pathsToOpen: [atom.getConfigDirPath()]})
    }
    this.refs.openDotAtom.addEventListener('click', openDotAtomClickHandler)
    this.disposables.add(new Disposable(() => this.refs.openDotAtom.removeEventListener('click', openDotAtomClickHandler)))

    this.addCorePanel('Core', 'settings', () => new GeneralPanel())
    this.addCorePanel('Editor', 'code', () => new EditorPanel())
    if (atom.config.getSchema('core.uriHandlerRegistration').type !== 'any') {
      // "feature flag" based on core support for URI handling
      this.addCorePanel('URI Handling', 'link', () => new UriHandlerPanel())
    }
    if ((process.platform === 'win32') && (require('atom').WinShell != null)) {
      const SystemPanel = require('./system-windows-panel')
      this.addCorePanel('System', 'device-desktop', () => new SystemPanel())
    }
    this.addCorePanel('Keybindings', 'keyboard', () => new KeybindingsPanel())
    this.addCorePanel('Packages', 'package', () => new InstalledPackagesPanel(this, this.packageManager))
    this.addCorePanel('Themes', 'paintcan', () => new ThemesPanel(this, this.packageManager))
    this.addCorePanel('Updates', 'cloud-download', () => new UpdatesPanel(this, this.packageManager))
    this.addCorePanel('Install', 'plus', () => new InstallPanel(this, this.packageManager))

    this.showDeferredPanel()

    if (!this.activePanel) {
      this.showPanel('Core')
    }

    if (document.body.contains(this.element)) {
      this.refs.sidebar.style.width = this.refs.sidebar.offsetWidth
    }
  }

  serialize () {
    return {
      deserializer: 'SettingsView',
      version: 2,
      activePanel: this.activePanel != null ? this.activePanel : this.deferredPanel,
      uri: this.uri
    }
  }

  getPackages () {
    let bundledPackageMetadataCache
    let left
    if (this.packages != null) { return this.packages }

    this.packages = atom.packages.getLoadedPackages()

    try {
      const packageMetadata = require(path.join(atom.getLoadSettings().resourcePath, 'package.json'))
      bundledPackageMetadataCache = packageMetadata ? packageMetadata._atomPackages : null
    } catch (error) {}

    // Include disabled packages so they can be re-enabled from the UI
    const disabledPackages = atom.config.get('core.disabledPackages') || []
    for (const packageName of disabledPackages) {
      var metadata
      const packagePath = atom.packages.resolvePackagePath(packageName)
      if (!packagePath) {
        continue
      }

      try {
        metadata = require(path.join(packagePath, 'package.json'))
      } catch (error) {
        if (bundledPackageMetadataCache && bundledPackageMetadataCache[packageName]) {
          metadata = bundledPackageMetadataCache[packageName].metadata
        }
      }
      if (metadata == null) {
        continue
      }

      const name = metadata.name != null ? metadata.name : packageName
      if (!_.findWhere(this.packages, {name})) {
        this.packages.push({name, metadata, path: packagePath})
      }
    }

    this.packages.sort((pack1, pack2) => {
      const title1 = this.packageManager.getPackageTitle(pack1)
      const title2 = this.packageManager.getPackageTitle(pack2)
      return title1.localeCompare(title2)
    })

    return this.packages
  }

  addCorePanel (name, iconName, panel) {
    const panelMenuItem = document.createElement('li')
    panelMenuItem.name = name
    panelMenuItem.setAttribute('name', name)

    const a = document.createElement('a')
    a.classList.add('icon', `icon-${iconName}`)
    a.textContent = name
    panelMenuItem.appendChild(a)

    this.refs.menuSeparator.parentElement.insertBefore(panelMenuItem, this.refs.menuSeparator)
    this.addPanel(name, panel)
  }

  addPanel (name, panelCreateCallback) {
    if (this.panelCreateCallbacks == null) {
      this.panelCreateCallbacks = {}
    }
    this.panelCreateCallbacks[name] = panelCreateCallback
    if (this.deferredPanel && this.deferredPanel.name === name) {
      this.showDeferredPanel()
    }
  }

  getOrCreatePanel (name, options) {
    let panel = this.panelsByName ? this.panelsByName[name] : null
    // These nested conditionals are not great but I feel like it's the most
    // expedient thing to do - I feel like the "right way" involves refactoring
    // this whole file.
    if (!panel) {
      let callback = this.panelCreateCallbacks ? this.panelCreateCallbacks[name] : null

      if (options && options.pack && !callback) {
        callback = () => {
          if (!options.pack.metadata) {
            const metadata = _.clone(options.pack)
            options.pack.metadata = metadata
          }
          return new PackageDetailView(options.pack, this, this.packageManager, this.snippetsProvider)
        }
      }

      if (callback) {
        panel = callback()
        if (this.panelsByName == null) {
          this.panelsByName = {}
        }
        this.panelsByName[name] = panel
        if (this.panelCreateCallbacks) {
          delete this.panelCreateCallbacks[name]
        }
      }
    }

    return panel
  }

  makePanelMenuActive (name) {
    const previouslyActivePanel = this.refs.sidebar.querySelector('.active')
    if (previouslyActivePanel) {
      previouslyActivePanel.classList.remove('active')
    }

    const newActivePanel = this.refs.sidebar.querySelector(`[name='${name}']`)
    if (newActivePanel) {
      newActivePanel.classList.add('active')
    }
  }

  focusActivePanel () {
    // Pass focus to panel that is currently visible
    for (let i = 0; i < this.refs.panels.children.length; i++) {
      const child = this.refs.panels.children[i]
      if (child.offsetWidth > 0) {
        child.focus()
      }
    }
  }

  showDeferredPanel () {
    if (this.deferredPanel) {
      const {name, options} = this.deferredPanel
      this.showPanel(name, options)
    }
  }

  // Public: show a panel.
  //
  // * `name` {String} the name of the panel to show
  // * `options` {Object} an options hash. Will be passed to `beforeShow()` on
  //   the panel. Options may include (but are not limited to):
  //   * `uri` the URI the panel was launched from
  showPanel (name, options) {
    const panel = this.getOrCreatePanel(name, options)
    if (panel) {
      this.appendPanel(panel, options)
      this.makePanelMenuActive(name)
      this.setActivePanel(name, options)
      this.deferredPanel = null
    } else {
      this.deferredPanel = {name, options}
    }
  }

  showPanelForURI (uri) {
    const regex = /config\/([a-z]+)\/?([a-zA-Z0-9_-]+)?/i
    const match = regex.exec(uri)

    if (match) {
      let panelName = match[1]
      panelName = panelName[0].toUpperCase() + panelName.slice(1)

      const options = {uri}
      if (panelName === 'Packages' && match[2] != null) {
        const packageName = match[2]
        panelName = packageName
        options.pack = {name: packageName}
        if (atom.packages.getLoadedPackage(packageName)) options.back = 'Packages'
      }
      this.showPanel(panelName, options)
    }
  }

  appendPanel (panel, options) {
    for (let i = 0; i < this.refs.panels.children.length; i++) {
      this.refs.panels.children[i].style.display = 'none'
    }

    if (!this.refs.panels.contains(panel.element)) {
      this.refs.panels.appendChild(panel.element)
    }

    if (panel.beforeShow) {
      panel.beforeShow(options)
    }
    panel.show()
    panel.focus()
  }

  setActivePanel (name, options = {}) {
    this.activePanel = {name, options}
  }

  removePanel (name) {
    const panel = this.panelsByName ? this.panelsByName[name] : null
    if (panel) {
      panel.destroy()
      delete this.panelsByName[name]
    }
  }

  getTitle () {
    return 'Settings'
  }

  getIconName () {
    return 'tools'
  }

  getURI () {
    return this.uri
  }

  isEqual (other) {
    return other instanceof SettingsView
  }

  scrollUp () {
    this.element.scrollTop -= document.body.offsetHeight / 20
  }

  scrollDown () {
    this.element.scrollTop += document.body.offsetHeight / 20
  }

  pageUp () {
    this.element.scrollTop -= this.element.offsetHeight
  }

  pageDown () {
    this.element.scrollTop += this.element.offsetHeight
  }

  scrollToTop () {
    this.element.scrollTop = 0
  }

  scrollToBottom () {
    this.element.scrollTop = this.element.scrollHeight
  }
}
<commit_msg>:art: Make function call param obvious in each case<commit_after>/** @babel */
/** @jsx etch.dom */

import path from 'path'
import etch from 'etch'
import _ from 'underscore-plus'
import {CompositeDisposable, Disposable} from 'atom'

import GeneralPanel from './general-panel'
import EditorPanel from './editor-panel'
import PackageDetailView from './package-detail-view'
import KeybindingsPanel from './keybindings-panel'
import InstallPanel from './install-panel'
import ThemesPanel from './themes-panel'
import InstalledPackagesPanel from './installed-packages-panel'
import UpdatesPanel from './updates-panel'
import UriHandlerPanel from './uri-handler-panel'
import PackageManager from './package-manager'

export default class SettingsView {
  constructor ({uri, packageManager, snippetsProvider, activePanel} = {}) {
    this.uri = uri
    this.packageManager = packageManager
    this.snippetsProvider = snippetsProvider
    this.deferredPanel = activePanel
    this.destroyed = false

    etch.initialize(this)
    this.disposables = new CompositeDisposable()
    this.disposables.add(atom.commands.add(this.element, {
      'core:move-up': () => { this.scrollUp() },
      'core:move-down': () => { this.scrollDown() },
      'core:page-up': () => { this.pageUp() },
      'core:page-down': () => { this.pageDown() },
      'core:move-to-top': () => { this.scrollToTop() },
      'core:move-to-bottom': () => { this.scrollToBottom() }
    }))
    process.nextTick(() => this.initializePanels())
  }

  update () {}

  destroy () {
    this.destroyed = true
    this.disposables.dispose()
    for (let name in this.panelsByName) {
      const panel = this.panelsByName[name]
      panel.destroy()
    }

    return etch.destroy(this)
  }

  render () {
    return (

            <button className='btn btn-default icon icon-link-external' ref='openDotAtom'>Open Config Folder</button>

        {/* The tabindex attr below ensures that clicks in a panel item won't
        cause this view to gain focus. This is important because when this view
        gains focus (e.g. immediately after atom displays it), it focuses the
        currently active panel item. If that focusing causes the active panel to
        scroll (e.g. because the active panel itself passes focus on to a search
        box at the top of a scrolled panel), then the browser will not fire the
        click event on the element within the panel on which the user originally
        clicked (e.g. a package card). This would prevent us from showing a
        package detail view when clicking on a package card. Phew! */}

    )
  }

  // This prevents the view being actually disposed when closed
  // If you remove it you will need to ensure the cached settingsView
  // in main.coffee is correctly released on close as well...
  onDidChangeTitle () { return new Disposable() }

  initializePanels () {
    if (this.refs.panels.children.length > 1) {
      return
    }

    this.panelsByName = {}
    const clickHandler = (event) => {
      const target = event.target.closest('.panels-menu li a, .panels-packages li a')
      if (target) {
        this.showPanel(target.closest('li').name)
      }
    }
    this.element.addEventListener('click', clickHandler)
    this.disposables.add(new Disposable(() => this.element.removeEventListener('click', clickHandler)))

    const focusHandler = () => {
      this.focusActivePanel()
    }
    this.element.addEventListener('focus', focusHandler)
    this.disposables.add(new Disposable(() => this.element.removeEventListener('focus', focusHandler)))

    const openDotAtomClickHandler = () => {
      atom.open({pathsToOpen: [atom.getConfigDirPath()]})
    }
    this.refs.openDotAtom.addEventListener('click', openDotAtomClickHandler)
    this.disposables.add(new Disposable(() => this.refs.openDotAtom.removeEventListener('click', openDotAtomClickHandler)))

    this.addCorePanel('Core', 'settings', () => new GeneralPanel())
    this.addCorePanel('Editor', 'code', () => new EditorPanel())
    if (atom.config.getSchema('core.uriHandlerRegistration').type !== 'any') {
      // "feature flag" based on core support for URI handling
      this.addCorePanel('URI Handling', 'link', () => new UriHandlerPanel())
    }
    if ((process.platform === 'win32') && (require('atom').WinShell != null)) {
      const SystemPanel = require('./system-windows-panel')
      this.addCorePanel('System', 'device-desktop', () => new SystemPanel())
    }
    this.addCorePanel('Keybindings', 'keyboard', () => new KeybindingsPanel())
    this.addCorePanel('Packages', 'package', () => new InstalledPackagesPanel(this, this.packageManager))
    this.addCorePanel('Themes', 'paintcan', () => new ThemesPanel(this, this.packageManager))
    this.addCorePanel('Updates', 'cloud-download', () => new UpdatesPanel(this, this.packageManager))
    this.addCorePanel('Install', 'plus', () => new InstallPanel(this, this.packageManager))

    this.showDeferredPanel()

    if (!this.activePanel) {
      this.showPanel('Core')
    }

    if (document.body.contains(this.element)) {
      this.refs.sidebar.style.width = this.refs.sidebar.offsetWidth
    }
  }

  serialize () {
    return {
      deserializer: 'SettingsView',
      version: 2,
      activePanel: this.activePanel != null ? this.activePanel : this.deferredPanel,
      uri: this.uri
    }
  }

  getPackages () {
    let bundledPackageMetadataCache
    let left
    if (this.packages != null) { return this.packages }

    this.packages = atom.packages.getLoadedPackages()

    try {
      const packageMetadata = require(path.join(atom.getLoadSettings().resourcePath, 'package.json'))
      bundledPackageMetadataCache = packageMetadata ? packageMetadata._atomPackages : null
    } catch (error) {}

    // Include disabled packages so they can be re-enabled from the UI
    const disabledPackages = atom.config.get('core.disabledPackages') || []
    for (const packageName of disabledPackages) {
      var metadata
      const packagePath = atom.packages.resolvePackagePath(packageName)
      if (!packagePath) {
        continue
      }

      try {
        metadata = require(path.join(packagePath, 'package.json'))
      } catch (error) {
        if (bundledPackageMetadataCache && bundledPackageMetadataCache[packageName]) {
          metadata = bundledPackageMetadataCache[packageName].metadata
        }
      }
      if (metadata == null) {
        continue
      }

      const name = metadata.name != null ? metadata.name : packageName
      if (!_.findWhere(this.packages, {name})) {
        this.packages.push({name, metadata, path: packagePath})
      }
    }

    this.packages.sort((pack1, pack2) => {
      const title1 = this.packageManager.getPackageTitle(pack1)
      const title2 = this.packageManager.getPackageTitle(pack2)
      return title1.localeCompare(title2)
    })

    return this.packages
  }

  addCorePanel (name, iconName, panel) {
    const panelMenuItem = document.createElement('li')
    panelMenuItem.name = name
    panelMenuItem.setAttribute('name', name)

    const a = document.createElement('a')
    a.classList.add('icon', `icon-${iconName}`)
    a.textContent = name
    panelMenuItem.appendChild(a)

    this.refs.menuSeparator.parentElement.insertBefore(panelMenuItem, this.refs.menuSeparator)
    this.addPanel(name, panel)
  }

  addPanel (name, panelCreateCallback) {
    if (this.panelCreateCallbacks == null) {
      this.panelCreateCallbacks = {}
    }
    this.panelCreateCallbacks[name] = panelCreateCallback
    if (this.deferredPanel && this.deferredPanel.name === name) {
      this.showDeferredPanel()
    }
  }

  getOrCreatePanel (name, options) {
    let panel = this.panelsByName ? this.panelsByName[name] : null
    // These nested conditionals are not great but I feel like it's the most
    // expedient thing to do - I feel like the "right way" involves refactoring
    // this whole file.
    if (!panel) {
      let callback = this.panelCreateCallbacks ? this.panelCreateCallbacks[name] : null

      if (options && options.pack && !callback) {
        callback = () => {
          if (!options.pack.metadata) {
            const metadata = _.clone(options.pack)
            options.pack.metadata = metadata
          }
          return new PackageDetailView(options.pack, this, this.packageManager, this.snippetsProvider)
        }
      }

      if (callback) {
        panel = callback()
        if (this.panelsByName == null) {
          this.panelsByName = {}
        }
        this.panelsByName[name] = panel
        if (this.panelCreateCallbacks) {
          delete this.panelCreateCallbacks[name]
        }
      }
    }

    return panel
  }

  makePanelMenuActive (name) {
    const previouslyActivePanel = this.refs.sidebar.querySelector('.active')
    if (previouslyActivePanel) {
      previouslyActivePanel.classList.remove('active')
    }

    const newActivePanel = this.refs.sidebar.querySelector(`[name='${name}']`)
    if (newActivePanel) {
      newActivePanel.classList.add('active')
    }
  }

  focusActivePanel () {
    // Pass focus to panel that is currently visible
    for (let i = 0; i < this.refs.panels.children.length; i++) {
      const child = this.refs.panels.children[i]
      if (child.offsetWidth > 0) {
        child.focus()
      }
    }
  }

  showDeferredPanel () {
    if (this.deferredPanel) {
      const {name, options} = this.deferredPanel
      this.showPanel(name, options)
    }
  }

  // Public: show a panel.
  //
  // * `name` {String} the name of the panel to show
  // * `options` {Object} an options hash. Will be passed to `beforeShow()` on
  //   the panel. Options may include (but are not limited to):
  //   * `uri` the URI the panel was launched from
  showPanel (name, options) {
    const panel = this.getOrCreatePanel(name, options)
    if (panel) {
      this.appendPanel(panel, options)
      this.makePanelMenuActive(name)
      this.setActivePanel(name, options)
      this.deferredPanel = null
    } else {
      this.deferredPanel = {name, options}
    }
  }

  showPanelForURI (uri) {
    const regex = /config\/([a-z]+)\/?([a-zA-Z0-9_-]+)?/i
    const match = regex.exec(uri)

    if (match) {
      const path1 = match[1]
      const path2 = match[2]

      if (path1 === "packages" && path2 != null) {
        this.showPanel(path2, {
          uri: uri,
          pack: {name: path2},
          back: atom.packages.getLoadedPackage(path2) ? "Packages" : null
        })
      } else {
        const panelName = path1[0].toUpperCase() + path1.slice(1)
        this.showPanel(panelName, {uri})
      }
    }
  }

  appendPanel (panel, options) {
    for (let i = 0; i < this.refs.panels.children.length; i++) {
      this.refs.panels.children[i].style.display = 'none'
    }

    if (!this.refs.panels.contains(panel.element)) {
      this.refs.panels.appendChild(panel.element)
    }

    if (panel.beforeShow) {
      panel.beforeShow(options)
    }
    panel.show()
    panel.focus()
  }

  setActivePanel (name, options = {}) {
    this.activePanel = {name, options}
  }

  removePanel (name) {
    const panel = this.panelsByName ? this.panelsByName[name] : null
    if (panel) {
      panel.destroy()
      delete this.panelsByName[name]
    }
  }

  getTitle () {
    return 'Settings'
  }

  getIconName () {
    return 'tools'
  }

  getURI () {
    return this.uri
  }

  isEqual (other) {
    return other instanceof SettingsView
  }

  scrollUp () {
    this.element.scrollTop -= document.body.offsetHeight / 20
  }

  scrollDown () {
    this.element.scrollTop += document.body.offsetHeight / 20
  }

  pageUp () {
    this.element.scrollTop -= this.element.offsetHeight
  }

  pageDown () {
    this.element.scrollTop += this.element.offsetHeight
  }

  scrollToTop () {
    this.element.scrollTop = 0
  }

  scrollToBottom () {
    this.element.scrollTop = this.element.scrollHeight
  }
}
<|endoftext|>


<commit_before>/*
 * Parsec
 * https://github.com/d-plaindoux/parsec
 *
 * Copyright (c) 2016 Didier Plaindoux
 * Licensed under the LGPL2 license.
 */

 module.exports = (function() {

     'use strict';

     var parser = require('../parsec/parser.js'),
         genlex = require('../genlex/genlex.js'),
         token = require('../genlex/token.js');

     //
     // Facilities
     //

     var tkNumber = token.parser.number,
         tkString = token.parser.string,
         tkKeyword = token.parser.keyword;

     function tkKey(s) {
         return tkKeyword.match(s);
     }

     /**
      * Json parser class
      */
     function JsonParser() {
         var keywords = [ "null", "false", "true", "{", "}", "[", "]", ":", "," ];
         this.tokenizer = genlex.generator(keywords).tokenBetweenSpaces(token.builder);
     }

     // unit -> Parser ? Token
     JsonParser.prototype.array = function() {
         var self = this,
             expr = function () { return self.expr(); },
             array = function () { return self.array(); };

         return parser.lazy(expr).
                thenLeft(tkKey(',').then(parser.lazy(array)).opt());
     };

     // unit -> Parser ? Token
     JsonParser.prototype.object = function() {
         var self = this,
             expr = function () { return self.expr(); },
             object = function () { return self.object(); };

         return tkString.thenLeft(tkKey(':')).then(parser.lazy(expr)).
                thenLeft(tkKey(',').then(parser.lazy(object)).opt());
     };

     // unit -> Parser ? Token
     JsonParser.prototype.expr = function() {
         var self = this,
             array = function () { return self.array(); },
             object = function () { return self.object(); };

         return tkNumber.
                or(tkString).
                or(tkKey("null")).
                or(tkKey("true")).
                or(tkKey("false")).
                or(tkKey('[').thenRight(parser.lazy(array).opt()).thenLeft(tkKey(']'))).
                or(tkKey('{').thenRight(parser.lazy(object).opt()).thenLeft(tkKey('}')));
     };

     // unit -> Parser ? char
     JsonParser.prototype.parser = function() {
        return this.tokenizer.chain(this.expr().thenLeft(parser.eos));
     };

     return {
         parse : function (source, index) {
             return new JsonParser().parser().parse(source, index);
         }
     };

}());<commit_msg>Simplify json parser code<commit_after>/*
 * Parsec
 * https://github.com/d-plaindoux/parsec
 *
 * Copyright (c) 2016 Didier Plaindoux
 * Licensed under the LGPL2 license.
 */

 module.exports = (function() {

     'use strict';

     var parser = require('../parsec/parser.js'),
         genlex = require('../genlex/genlex.js'),
         token = require('../genlex/token.js');

     //
     // Facilities
     //

     var tkNumber = token.parser.number,
         tkString = token.parser.string,
         tkKeyword = token.parser.keyword;

     function tkKey(s) {
         return tkKeyword.match(s);
     }

     /**
      * Json parser class
      */
     function JsonParser() {
         var keywords = [ "null", "false", "true", "{", "}", "[", "]", ":", "," ];
         this.tokenizer = genlex.generator(keywords).tokenBetweenSpaces(token.builder);
     }

     // unit -> Parser ? Token
     function array() {
         return parser.lazy(expr).
                thenLeft(tkKey(',').then(parser.lazy(array)).opt());
     }

     // unit -> Parser ? Token
     function object() {
         return tkString.thenLeft(tkKey(':')).then(parser.lazy(expr)).
                thenLeft(tkKey(',').then(parser.lazy(object)).opt());
     }

     // unit -> Parser ? Token
     function expr() {
         return tkNumber.
                or(tkString).
                or(tkKey("null")).
                or(tkKey("true")).
                or(tkKey("false")).
                or(tkKey('[').thenRight(parser.lazy(array).opt()).thenLeft(tkKey(']'))).
                or(tkKey('{').thenRight(parser.lazy(object).opt()).thenLeft(tkKey('}')));
     }

     // unit -> Parser ? char
     JsonParser.prototype.parser = function() {
        return this.tokenizer.chain(expr().thenLeft(parser.eos));
     };

     return {
         parse : function (source, index) {
             return new JsonParser().parser().parse(source, index);
         }
     };

}());<|endoftext|>


<commit_before>// state_machine.js
//  - Interaction machine for messages and events.

var jed = require("jed");

var promise = require("./promise.js");
var Promise = promise.Promise;
var maybe_promise = promise.maybe_promise;
var success = promise.success;

var metrics = require("./metrics.js");
var MetricStore = metrics.MetricStore;

var states = require("./states");
var StateError = states.StateError;

function IMEvent(event, im, data) {

    var self = this;

    self.event = event; // event type
    self.im = im; // interaction machine
    self.data = data; // event data
}

function ConfigReadEvent(im, config) {
    return new IMEvent('config_read', im, {config: config});
}

function SessionNewEvent(im) {
    return new IMEvent('session_new', im, {});
}

function SessionCloseEvent(im, possible_timeout) {
    return new IMEvent('session_close', im,
                       {possible_timeout: possible_timeout});
}

function SessionResumeEvent(im) {
    return new IMEvent('session_resume', im, {});
}

function NewUserEvent(im, user_addr, user) {
    return new IMEvent('new_user', im, {user_addr: user_addr, user: user});
}

function StateEnterEvent(im, state) {
    return new IMEvent('state_enter', im, {state: state});
}

function StateExitEvent(im, state, user) {
    return new IMEvent('state_exit', im, {state:state});
}

function InboundEventEvent(im, event) {
    return new IMEvent('inbound_event', im, {event: event});
}

function InteractionMachine(api, state_creator) {

    var self = this;

    self.api = api;
    self.state_creator = state_creator;

    self.user = null;
    self.user_addr = null;
    self.i18n = null;
    self.i18n_lang = null;
    self.current_state = null;
    self.config = null;
    self.metrics = null;

    self.set_user_lang = function(lang) {
        self.user.lang = lang;
    };

    self.set_user_answer = function(state_name, answer) {
        if (!self.user.answers) self.user.answers = {};
        self.user.answers[state_name] = answer;
    };

    self.get_user_answer = function(state_name) {
        if (!self.user.answers) return null;
        var answer = self.user.answers[state_name];
        return (typeof answer == 'undefined') ? null : answer;
    };

    self.set_user_state = function(state_name) {
        // this just sets the users state
        // actual state changes are handled by switch_state
        self.user.current_state = state_name;
    };

    self.attach = function() {
        self.api.on_unknown_command = self.on_unknown_command;
        self.api.on_inbound_message = self.on_inbound_message;
        self.api.on_inbound_event = self.on_inbound_event;
        self.api.im = self;
    };

    self.on_event = function(event) {
        return self.state_creator.on_event(event);
    };

    self.switch_state = function(state_name) {
        var p = new Promise();
        if (self.current_state && state_name == self.current_state.name) {
            p.callback();
            return p;
        }
        var old_state_name = (self.current_state ? self.current_state.name :
                              self.user.current_state);
        p.add_callback(self.refresh_i18n);
        p.add_callback(function () {
            return self.state_creator.switch_state(state_name, self);
        });
        p.add_callback(function (new_state) {
            var ev_p;
            if (self.current_state) {
                ev_p = self.on_event(new StateExitEvent(self,
                                                        self.current_state));
            } else {
                ev_p = success();
            }
            var on_exit = (self.current_state ?
                           self.current_state.on_exit :
                           function () {});
            var on_enter = (old_state_name != new_state.name ?
                            new_state.on_enter :
                            function () {});
            ev_p.add_callback(function () {
                return maybe_promise(on_exit());
            });
            ev_p.add_callback(function () {
                self.current_state = new_state;
                self.user.current_state = new_state.name;
                return self.on_event(new StateEnterEvent(self, new_state));
            });
            ev_p.add_callback(function() {
                return maybe_promise(new_state.setup_state(self));
            });
            ev_p.add_callback(function() {
                return maybe_promise(on_enter());
            });
            return ev_p;
        });
        p.callback();
        return p;
    };

    self.refresh_i18n = function() {
        var p = new Promise();
        var lang = self.user.lang || self.config.default_lang;
        if (!lang) {
            self.i18n_lang = null;
            self.i18n = new jed({});
            p.callback();
            return p;
        }
        if ((!self.i18n) || (lang != self.i18n_lang)) {
            self.fetch_translation(lang, function (i18n) {
                self.i18n_lang = lang;
                self.i18n = i18n;
                p.callback();
            });
            return p;
        }
        p.callback(); // cached case
        return p;
    };

    self.fetch_config_value = function(key, json, done) {
        self.api.request("config.get", {key: key},
            function(reply) {
                var value = null;
                if (reply.value && json) {
                    try { value = JSON.parse(reply.value); }
                    catch (e) {}
                }
                else {
                    value = reply.value;
                }
                done(value);
            });
    };

    self.fetch_translation = function(lang, done) {
        self.fetch_config_value("translation." + lang, true,
            function(domain_data) {
                var jed_data = {};
                if (domain_data) {
                    jed_data.domain = "messages";
                    jed_data.locale_data = {"messages": domain_data};
                }
                var i18n = new jed(jed_data);
                done(i18n);
            });
    };

    self.setup_config = function() {
        var p = new Promise();
        self.fetch_config_value("config", true,
            function (config) {
                self.config = config || {};
                p.callback();
            });
        p.add_callback(function (reply) {
            return self.on_event(new ConfigReadEvent(self, self.config));
        });
        return p;
    };

    self.setup_metrics = function() {
        var store = self.config.metric_store || 'default';
        self.metrics = new MetricStore(self.api, store);
    };

    self.user_key = function(from_addr) {
        var prefix = (self.config.user_store ?
                      self.config.user_store + '.' : '');
        return "users." + prefix + from_addr;
    };

    self.load_user = function(from_addr, done) {
        var p = new Promise();
        self.user_addr = from_addr;
        self.api.request("kv.get",
            {key: self.user_key(from_addr)},
            function(reply) {
                p.callback(reply);
            });
        p.add_callback(function (reply) {
            self.user = reply.value || {};
            if (!reply.value) {
                return self.on_event(new NewUserEvent(self,
                                                      self.user_addr,
                                                      self.user));
            }
        });
        return p;
    };

    self.store_user = function(from_addr, user) {
        var p = new Promise();
        self.api.request("kv.set",
            {
                key: self.user_key(from_addr),
                value: user
            },
            function(reply) {
                p.callback();
            });
        return p;
    };

    self.on_unknown_command = function(cmd) {
        self.log("Received unknown command: " + JSON.stringify(cmd));
        self.done();
    };

    self.log = function(message) { self.api.log_info(message); };
    self.done = function() { self.api.done(); };

    self.api_request = function(cmd_name, cmd) {
        var p = new Promise();
        self.api.request(cmd_name, cmd, p.callback);
        return p;
    };

    self.reply = function(msg, save_user) {
        var p = self.switch_state(self.user.current_state);

        p.add_callback(function() {
            var continue_session = self.current_state.continue_session();
            if (!continue_session) {
                return self.on_event(new SessionCloseEvent(self, false));
            }
        });

        if (save_user) {
            p.add_callback(function() {
                self.log("Ending in state: " + self.current_state.name);
                self.log("Saving user: " + JSON.stringify(self.user));
                self.store_user(msg.from_addr, self.user);
            });
        }

        p.add_callback(function () {
            var continue_session = self.current_state.continue_session();
            self.api_request("outbound.reply_to", {
                content: self.current_state.display(),
                in_reply_to: msg.message_id,
                continue_session: continue_session
            });
        });

        return p;
    };

    self.on_inbound_message = function(cmd) {
        var msg = cmd.msg;
        var p = new Promise();

        p.add_callback(self.setup_config);
        p.add_callback(self.setup_metrics);
        p.add_callback(function () {
            return self.load_user(msg.from_addr);
        });
        p.add_callback(function () {
            if (msg.content == "!restart") {
                self.user = {};
                msg.content = "";
            }
            self.log("Loaded user " + self.user_addr + ": " + JSON.stringify(self.user));
            self.log("Content: " + msg.content);
            return self.switch_state(self.user.current_state);
        });
        p.add_callback(function () {
            self.log("Switched to state: " + self.current_state.name);
            if (msg.session_event == "close") {
                var ev_p = self.on_event(new SessionCloseEvent(self, true));
                ev_p.add_callback(function () {
                    return self.store_user(msg.from_addr, self.user);
                });
                return ev_p;
            }
            else if (msg.session_event == "new") {
                var ev_p = self.on_event(new SessionNewEvent(self));
                ev_p.add_callback(function () {
                    var state_done = new Promise();
                    self.current_state.new_session_event(function () {
                        state_done.callback();
                    });
                    return state_done;
                });
                ev_p.add_callback(function () {
                    return self.reply(msg, true);
                });
                return ev_p;
            }
            else {
                var ev_p = self.on_event(new SessionResumeEvent(self));
                if (msg.content) {
                    ev_p.add_callback(function() {
                        var state_done = new Promise();
                        self.current_state.input_event(
                            msg.content,
                            function () {
                                state_done.callback();
                            });
                        return state_done;
                    });
                }
                ev_p.add_callback(function() {
                    return self.reply(msg, true);
                });
                return ev_p;
            }
        });
        p.add_callback(self.done);

        p.callback();
    };

    self.on_inbound_event = function(cmd) {
        var event = cmd.msg;

        var p = new Promise();
        p.add_callback(function() {
            return self.on_event(new InboundEventEvent(self, event));
        });
        p.add_callback(self.done);
        p.callback();
    };

}

function StateCreator(start_state) {
    var self = this;

    self.start_state = start_state;
    self.state_creators = {};

    self.on_event = function(event) {
        var handler = self['on_' + event.event];
        if (typeof handler != 'undefined') {
            return maybe_promise(handler(event));
        }
        return success();
    };

    self.add_creator = function(state_name, state_creation_function) {
        if (self.state_creators[state_name]) {
            throw new StateError("Duplicate state '" + state_name + "'");
        }
        self.state_creators[state_name] = state_creation_function;
    };

    self.add_state = function(state, translate) {
        translate = (typeof translate == 'undefined') ? true : translate;
        self.add_creator(state.name, function(state_name, im) {
            if (translate) {
                state.translate(im.i18n);
            }
            return state;
        });
    };

    self.switch_state = function(state_name, im) {
        if (typeof state_name == "undefined")
            state_name = self.start_state;
        if (!self.state_creators[state_name]) {
            throw new StateError("Unknown state '" + state_name + "'");
        }
        var creator = self.state_creators[state_name];
        return maybe_promise(creator.call(self, state_name, im));
    };
}

// export

this.InteractionMachine = InteractionMachine;
this.StateCreator = StateCreator;<commit_msg>When a user is in an unknown state, push them to the start state rather than raising an error and locking them out of the application entirely.<commit_after>// state_machine.js
//  - Interaction machine for messages and events.

var jed = require("jed");

var promise = require("./promise.js");
var Promise = promise.Promise;
var maybe_promise = promise.maybe_promise;
var success = promise.success;

var metrics = require("./metrics.js");
var MetricStore = metrics.MetricStore;

var states = require("./states");
var StateError = states.StateError;

function IMEvent(event, im, data) {

    var self = this;

    self.event = event; // event type
    self.im = im; // interaction machine
    self.data = data; // event data
}

function ConfigReadEvent(im, config) {
    return new IMEvent('config_read', im, {config: config});
}

function SessionNewEvent(im) {
    return new IMEvent('session_new', im, {});
}

function SessionCloseEvent(im, possible_timeout) {
    return new IMEvent('session_close', im,
                       {possible_timeout: possible_timeout});
}

function SessionResumeEvent(im) {
    return new IMEvent('session_resume', im, {});
}

function NewUserEvent(im, user_addr, user) {
    return new IMEvent('new_user', im, {user_addr: user_addr, user: user});
}

function StateEnterEvent(im, state) {
    return new IMEvent('state_enter', im, {state: state});
}

function StateExitEvent(im, state, user) {
    return new IMEvent('state_exit', im, {state:state});
}

function InboundEventEvent(im, event) {
    return new IMEvent('inbound_event', im, {event: event});
}

function InteractionMachine(api, state_creator) {

    var self = this;

    self.api = api;
    self.state_creator = state_creator;

    self.user = null;
    self.user_addr = null;
    self.i18n = null;
    self.i18n_lang = null;
    self.current_state = null;
    self.config = null;
    self.metrics = null;

    self.set_user_lang = function(lang) {
        self.user.lang = lang;
    };

    self.set_user_answer = function(state_name, answer) {
        if (!self.user.answers) self.user.answers = {};
        self.user.answers[state_name] = answer;
    };

    self.get_user_answer = function(state_name) {
        if (!self.user.answers) return null;
        var answer = self.user.answers[state_name];
        return (typeof answer == 'undefined') ? null : answer;
    };

    self.set_user_state = function(state_name) {
        // this just sets the users state
        // actual state changes are handled by switch_state
        self.user.current_state = state_name;
    };

    self.attach = function() {
        self.api.on_unknown_command = self.on_unknown_command;
        self.api.on_inbound_message = self.on_inbound_message;
        self.api.on_inbound_event = self.on_inbound_event;
        self.api.im = self;
    };

    self.on_event = function(event) {
        return self.state_creator.on_event(event);
    };

    self.switch_state = function(state_name) {
        var p = new Promise();
        if (self.current_state && state_name == self.current_state.name) {
            p.callback();
            return p;
        }
        var old_state_name = (self.current_state ? self.current_state.name :
                              self.user.current_state);
        p.add_callback(self.refresh_i18n);
        p.add_callback(function () {
            return self.state_creator.switch_state(state_name, self);
        });
        p.add_callback(function (new_state) {
            var ev_p;
            if (self.current_state) {
                ev_p = self.on_event(new StateExitEvent(self,
                                                        self.current_state));
            } else {
                ev_p = success();
            }
            var on_exit = (self.current_state ?
                           self.current_state.on_exit :
                           function () {});
            var on_enter = (old_state_name != new_state.name ?
                            new_state.on_enter :
                            function () {});
            ev_p.add_callback(function () {
                return maybe_promise(on_exit());
            });
            ev_p.add_callback(function () {
                self.current_state = new_state;
                self.user.current_state = new_state.name;
                return self.on_event(new StateEnterEvent(self, new_state));
            });
            ev_p.add_callback(function() {
                return maybe_promise(new_state.setup_state(self));
            });
            ev_p.add_callback(function() {
                return maybe_promise(on_enter());
            });
            return ev_p;
        });
        p.callback();
        return p;
    };

    self.refresh_i18n = function() {
        var p = new Promise();
        var lang = self.user.lang || self.config.default_lang;
        if (!lang) {
            self.i18n_lang = null;
            self.i18n = new jed({});
            p.callback();
            return p;
        }
        if ((!self.i18n) || (lang != self.i18n_lang)) {
            self.fetch_translation(lang, function (i18n) {
                self.i18n_lang = lang;
                self.i18n = i18n;
                p.callback();
            });
            return p;
        }
        p.callback(); // cached case
        return p;
    };

    self.fetch_config_value = function(key, json, done) {
        self.api.request("config.get", {key: key},
            function(reply) {
                var value = null;
                if (reply.value && json) {
                    try { value = JSON.parse(reply.value); }
                    catch (e) {}
                }
                else {
                    value = reply.value;
                }
                done(value);
            });
    };

    self.fetch_translation = function(lang, done) {
        self.fetch_config_value("translation." + lang, true,
            function(domain_data) {
                var jed_data = {};
                if (domain_data) {
                    jed_data.domain = "messages";
                    jed_data.locale_data = {"messages": domain_data};
                }
                var i18n = new jed(jed_data);
                done(i18n);
            });
    };

    self.setup_config = function() {
        var p = new Promise();
        self.fetch_config_value("config", true,
            function (config) {
                self.config = config || {};
                p.callback();
            });
        p.add_callback(function (reply) {
            return self.on_event(new ConfigReadEvent(self, self.config));
        });
        return p;
    };

    self.setup_metrics = function() {
        var store = self.config.metric_store || 'default';
        self.metrics = new MetricStore(self.api, store);
    };

    self.user_key = function(from_addr) {
        var prefix = (self.config.user_store ?
                      self.config.user_store + '.' : '');
        return "users." + prefix + from_addr;
    };

    self.load_user = function(from_addr, done) {
        var p = new Promise();
        self.user_addr = from_addr;
        self.api.request("kv.get",
            {key: self.user_key(from_addr)},
            function(reply) {
                p.callback(reply);
            });
        p.add_callback(function (reply) {
            self.user = reply.value || {};
            if (!reply.value) {
                return self.on_event(new NewUserEvent(self,
                                                      self.user_addr,
                                                      self.user));
            }
        });
        return p;
    };

    self.store_user = function(from_addr, user) {
        var p = new Promise();
        self.api.request("kv.set",
            {
                key: self.user_key(from_addr),
                value: user
            },
            function(reply) {
                p.callback();
            });
        return p;
    };

    self.on_unknown_command = function(cmd) {
        self.log("Received unknown command: " + JSON.stringify(cmd));
        self.done();
    };

    self.log = function(message) { self.api.log_info(message); };
    self.done = function() { self.api.done(); };

    self.api_request = function(cmd_name, cmd) {
        var p = new Promise();
        self.api.request(cmd_name, cmd, p.callback);
        return p;
    };

    self.reply = function(msg, save_user) {
        var p = self.switch_state(self.user.current_state);

        p.add_callback(function() {
            var continue_session = self.current_state.continue_session();
            if (!continue_session) {
                return self.on_event(new SessionCloseEvent(self, false));
            }
        });

        if (save_user) {
            p.add_callback(function() {
                self.log("Ending in state: " + self.current_state.name);
                self.log("Saving user: " + JSON.stringify(self.user));
                self.store_user(msg.from_addr, self.user);
            });
        }

        p.add_callback(function () {
            var continue_session = self.current_state.continue_session();
            self.api_request("outbound.reply_to", {
                content: self.current_state.display(),
                in_reply_to: msg.message_id,
                continue_session: continue_session
            });
        });

        return p;
    };

    self.on_inbound_message = function(cmd) {
        var msg = cmd.msg;
        var p = new Promise();

        p.add_callback(self.setup_config);
        p.add_callback(self.setup_metrics);
        p.add_callback(function () {
            return self.load_user(msg.from_addr);
        });
        p.add_callback(function () {
            if (msg.content == "!restart") {
                self.user = {};
                msg.content = "";
            }
            self.log("Loaded user " + self.user_addr + ": " + JSON.stringify(self.user));
            self.log("Content: " + msg.content);
            return self.switch_state(self.user.current_state);
        });
        p.add_callback(function () {
            self.log("Switched to state: " + self.current_state.name);
            if (msg.session_event == "close") {
                var ev_p = self.on_event(new SessionCloseEvent(self, true));
                ev_p.add_callback(function () {
                    return self.store_user(msg.from_addr, self.user);
                });
                return ev_p;
            }
            else if (msg.session_event == "new") {
                var ev_p = self.on_event(new SessionNewEvent(self));
                ev_p.add_callback(function () {
                    var state_done = new Promise();
                    self.current_state.new_session_event(function () {
                        state_done.callback();
                    });
                    return state_done;
                });
                ev_p.add_callback(function () {
                    return self.reply(msg, true);
                });
                return ev_p;
            }
            else {
                var ev_p = self.on_event(new SessionResumeEvent(self));
                if (msg.content) {
                    ev_p.add_callback(function() {
                        var state_done = new Promise();
                        self.current_state.input_event(
                            msg.content,
                            function () {
                                state_done.callback();
                            });
                        return state_done;
                    });
                }
                ev_p.add_callback(function() {
                    return self.reply(msg, true);
                });
                return ev_p;
            }
        });
        p.add_callback(self.done);

        p.callback();
    };

    self.on_inbound_event = function(cmd) {
        var event = cmd.msg;

        var p = new Promise();
        p.add_callback(function() {
            return self.on_event(new InboundEventEvent(self, event));
        });
        p.add_callback(self.done);
        p.callback();
    };

}

function StateCreator(start_state) {
    var self = this;

    self.start_state = start_state;
    self.state_creators = {};

    self.on_event = function(event) {
        var handler = self['on_' + event.event];
        if (typeof handler != 'undefined') {
            return maybe_promise(handler(event));
        }
        return success();
    };

    self.add_creator = function(state_name, state_creation_function) {
        if (self.state_creators[state_name]) {
            throw new StateError("Duplicate state '" + state_name + "'");
        }
        self.state_creators[state_name] = state_creation_function;
    };

    self.add_state = function(state, translate) {
        translate = (typeof translate == 'undefined') ? true : translate;
        self.add_creator(state.name, function(state_name, im) {
            if (translate) {
                state.translate(im.i18n);
            }
            return state;
        });
    };

    self.switch_state = function(state_name, im) {
        if (typeof state_name == "undefined")
            // handles new users who have no current state
            state_name = self.start_state;
        if (!self.state_creators[state_name]) {
            // handles users who somehow have an unknown state
            // (possibly they are in a state from a previous version
            // of the js application).
            im.log("Unknown state '" + state_name + "'. Switch to" +
                   " start state, '" + self.start_state + "'.");
            state_name = self.start_state;
        }
        if (!self.state_creators[state_name]) {
            // the only way to arrive here should be if the start state
            // is not a valid state.
            throw new StateError("Unknown start state '" + state_name + "'");
        }
        var creator = self.state_creators[state_name];
        return maybe_promise(creator.call(self, state_name, im));
    };
}

// export

this.InteractionMachine = InteractionMachine;
this.StateCreator = StateCreator;<|endoftext|>