<commit_before>'use strict'

/**
 * This file is for parsing Markdown to HTML
 */

let katex = require("katex"), // for katex
    // highlightjs = require("highlight.js"), // for highlight
    cheerio = require("cheerio"),
    path = require('path'),
    remarkable = require('remarkable'),
    uslug = require('uslug'),
    // Highlights = require('highlights'),
    Highlights = require(path.join(atom.getLoadSettings().resourcePath, 'node_modules/highlights/lib/highlights.js')),
    scopeForLanguageName = require('./extension-helper.js').scopeForLanguageName,
    toc = require('markdown-toc')

let defaults = {
  html:         true,        // Enable HTML tags in source
  xhtmlOut:     false,        // Use '/' to close single tags (
)
  breaks:       true,        // Convert '\n' in paragraphs into

  langPrefix:   'language-',  // CSS language prefix for fenced blocks
  linkify:      true,         // autoconvert URL-like texts to links
  linkTarget:   '',           // set target to open link in
  typographer:  true,         // Enable smartypants and other sweet transforms
}

let md = new remarkable(defaults)

// inline MATH rule
// $...$
// $$...$$
md.inline.ruler.before('autolink', 'math', function(state, silent) {
  if (state.src[state.pos] !== '$') return false

  let content = null
  let tag = '$'
  if (state.src.startsWith('$$', state.pos)) {
    tag = '$$'
  }

  let end = state.src.indexOf(tag, state.pos + tag.length)
  if (end >= 0) {
    content = state.src.slice(state.pos + tag.length, end)
  } else {
    return false
  }

  if (content && !silent) {
    state.push({
      type: 'math',
      content: content,
      tag: tag
    })
    state.pos += (content.length + 2 * tag.length)
    return true
  }
  return false
})

md.renderer.rules.math = function(tokens, idx) {
  let content = tokens[idx].content,
      tag = tokens[idx].tag

  if (!content) return

  try {
    return  katex.renderToString(content, {
              displayMode: (tag === '$' ? false : true)
            })
  } catch (e) {
    return '{ parse error: ' + content + ' }'
  }
}

// custom-comment
md.block.ruler.before('code', 'custom-comment', function(state, start, end, silent) {
  let pos = state.bMarks[start] + state.tShift[start],
      max = state.eMarks[start]
  if (pos >= max) return false
  if (state.src.startsWith('', pos + 4)
    if (end >= 0) {
      let content = state.src.slice(pos + 4, end).trim()
      state.tokens.push({
        type: 'custom',
        content: content
      })
      state.line = start + 1
      return true
    } else {
      return false
    }
  }
})

// Settings
atom.config.observe('atom-markdown-katex.breakOnSingleNewline', (breakOnSingleNewline)=> {
  md.set({breaks: breakOnSingleNewline})
})

//
// Inject line numbers for sync scroll. Notes:
//
// - We track only headings and paragraphs on first level. That's enougth.
// - Footnotes content causes jumps. Level limit filter it automatically.
//
// YIYI : 这里我不仅仅 map 了 level 0
md.renderer.rules.paragraph_open = function (tokens, idx) {
  let line = null
  if (tokens[idx].lines /*&& tokens[idx].level === 0*/) {
    line = tokens[idx].lines[0]
    return '
'
  }
  return '
'
}

// Build offsets for each line (lines can be wrapped)
// That's a bit dirty to process each line everytime, but ok for demo.
// Optimizations are required only for big texts.
function buildScrollMap(markdownPreview) {
  var i, offset, nonEmptyList, pos, a, b, lineHeightMap, linesCount, acc, _scrollMap

  let editor = markdownPreview.editor
  let markdownHtmlView = markdownPreview.htmlView
  let lines = editor.getBuffer().getLines()

  // offset = $('.result-html').scrollTop() - $('.result-html').offset().top;
  _scrollMap = []
  nonEmptyList = []
  lineHeightMap = []

  acc = 0;
  lines.forEach(function(str, n) {
    lineHeightMap.push(editor.screenRowForBufferRow(n))
  })
  linesCount = editor.getScreenLineCount()

  for (let i = 0; i < linesCount; i++) { _scrollMap.push(-1); }

  nonEmptyList.push(0)
  _scrollMap[0] = 0

  // 把有标记 data-line 的 element 的 offsetTop 记录到 _scrollMap
  let lineElements = markdownHtmlView.querySelectorAll ('.line')
  for (let i = 0; i < lineElements.length; i++) {
    let el = lineElements[i],
        t = el.getAttribute('data-line')
    if (t === '') { return; }
    t = lineHeightMap[Number(t)] // get screen buffer row
    if (t !== 0) { nonEmptyList.push(t) }
    _scrollMap[t] = Math.round(el.offsetTop)
  }

  nonEmptyList.push(linesCount)
  _scrollMap[linesCount] = markdownHtmlView.scrollHeight;

  pos = 0
  for (i = 1; i < linesCount; i++) {
    if (_scrollMap[i] !== -1) {
      pos++
      continue
    }

    a = nonEmptyList[pos]
    b = nonEmptyList[pos + 1]
    _scrollMap[i] = Math.round((_scrollMap[b] * (i - a) + _scrollMap[a] * (b - i)) / (b - a))
  }

  return _scrollMap  // scrollMap's length == screenLineCount
}

// resolve image path and pre code block...
function resolveImagePathAndCodeBlock(html, directoryPath) {
  if (!directoryPath) return

  let $ = cheerio.load(html)
  $('img').each(function(i, imgElement) {
    let img = $(imgElement)
    let src = img.attr('src')
    if (src &&
      (!(src.startsWith('http://') ||
        src.startsWith('https://') ||
        src.startsWith('atom://')  ||
        src.startsWith('file://'))) &&
      (src.startsWith('./') ||
        src.startsWith('../') ||
        src[0] !== '/')) {
      img.attr('src', path.resolve(directoryPath,  src))
    }
  })

  $('pre').each(function(i, preElement) {
    let codeBlock = $(preElement).children().first()
    let lang = 'text'
    if (codeBlock.attr('class')) {
      lang = codeBlock.attr('class').replace(/^language-/, '') || 'text'
    }

    let highlighter = new Highlights({registry: atom.grammars}),
        html = highlighter.highlightSync({
          fileContents: codeBlock.text(),
          scopeName: scopeForLanguageName(lang),
        })

    let highlightedBlock = $(html)

    highlightedBlock.removeClass('editor').addClass('lang-' + lang)

    $(preElement).replaceWith(highlightedBlock)
  })

  return $.html()
}

// parse markdown content to html
function parseMD(markdownPreview) {
  let editor = markdownPreview.editor
  let rootDirectoryPath = markdownPreview.rootDirectoryPath

  let inputString = markdownPreview.textContent
  let headings = []

  // replace math expression
  let useKaTeX = atom.config.get('atom-markdown-katex.useKaTeX')

  let tocNeedUpdate = false
  let tocTable = {} // eliminate repeated slug
  let tocEnabled = false

  // overwrite remark heading parse function
  md.renderer.rules.heading_open = (tokens, idx)=> {
    let line = null,
        id = null

    if (tokens[idx + 1] && tokens[idx + 1].content) {
      id = uslug(tokens[idx + 1].content)
      if (tocTable[id] >= 0) {
        tocTable[id] += 1
        id = id + '-' + tocTable[id]
      } else {
        tocTable[id] = 0
      }
      if (!tocNeedUpdate) {
        headings.push(tokens[idx + 1].content)
      }
    }

    if (tokens[idx].lines /*&& tokens[idx].level === 0*/) {
      line = tokens[idx].lines[0]
      return `<h${tokens[idx].hLevel} class="line" data-line="${line}" ${id ? `id="${id}"` : ''}>`
    }
    return `<h${tokens[idx].hLevel} ${id ? `id="${id}"` : ''}>`
  }

  //
  md.renderer.rules.custom = (tokens, idx)=> {
    let content = tokens[idx].content

    if (content === 'pagebreak' || content === 'newpage') {
      return '

'
    } else if (content === 'toc') {
      tocEnabled = true
    }
    return ''
  }

  let html = md.render(inputString)

  // check toc update
  if (tocEnabled) {
    let oldHeadingsLength = markdownPreview.headings.length
    let newHeadingsLength = headings.length
    if (oldHeadingsLength !== newHeadingsLength) {
      markdownPreview.headings = headings
      tocNeedUpdate = true
    } else {
      for (let i = 0; i < headings.length; i++) {
        if (markdownPreview.headings[i] !== headings[i]) {
          markdownPreview.headings = headings
          tocNeedUpdate = true
          break
        }
      }
    }

    if (tocNeedUpdate) {
      console.log('toc update')
      let cursorPos = editor.getCursorScreenPosition()

      let replaced = toc.insert(inputString, {slugify: uslug})

      tocTable = {} // reset tocTable before md
      tocEnabled = false
      html = md.render(replaced)
      markdownPreview.editor.setText(replaced)

      markdownPreview.parseDelay = Date.now() + 500 // prevent render again
      markdownPreview.editorScrollDelay = Date.now() + 500
      markdownPreview.previewScrollDelay = Date.now() + 500

      // set cursor position
      if (newHeadingsLength > oldHeadingsLength) {
        editor.setCursorScreenPosition([cursorPos.row + 1, cursorPos.column])
      } else if (newHeadingsLength < oldHeadingsLength) {
        editor.setCursorScreenPosition([cursorPos.row - 1, cursorPos.column])
      } else {
        editor.setCursorScreenPosition([cursorPos.row, cursorPos.column])
      }
    }
  } else {
    markdownPreview.headings = headings
  }

  return resolveImagePathAndCodeBlock(html, rootDirectoryPath)
}

module.exports = {
  parseMD,
  buildScrollMap
}
<commit_msg>fix a math bug<commit_after>'use strict'

/**
 * This file is for parsing Markdown to HTML
 */

let katex = require("katex"), // for katex
    cheerio = require("cheerio"),
    path = require('path'),
    remarkable = require('remarkable'),
    uslug = require('uslug'),
    Highlights = require(path.join(atom.getLoadSettings().resourcePath, 'node_modules/highlights/lib/highlights.js')),
    scopeForLanguageName = require('./extension-helper.js').scopeForLanguageName,
    toc = require('markdown-toc')

let defaults = {
  html:         true,        // Enable HTML tags in source
  xhtmlOut:     false,        // Use '/' to close single tags (
)
  breaks:       true,        // Convert '\n' in paragraphs into

  langPrefix:   'language-',  // CSS language prefix for fenced blocks
  linkify:      true,         // autoconvert URL-like texts to links
  linkTarget:   '',           // set target to open link in
  typographer:  true,         // Enable smartypants and other sweet transforms
}

let md = new remarkable(defaults)

// inline MATH rule
// $...$
// $$...$$
md.inline.ruler.before('autolink', 'math', function(state, silent) {
  if (state.src[state.pos] !== '$') return false

  let content = null
  let tag = '$'
  if (state.src.startsWith('$$', state.pos)) {
    tag = '$$'
  }

  let end = -1
  for (let i = state.pos + tag.length; i < state.src.length; i++) {
    if (state.src[i] === '\\') {
      i++
    } else if (state.src.startsWith(tag, i)) {
      end = i
      break
    }
  }

  if (end >= 0) {
    content = state.src.slice(state.pos + tag.length, end)
  } else {
    return false
  }

  if (content && !silent) {
    state.push({
      type: 'math',
      content: content,
      tag: tag
    })
    state.pos += (content.length + 2 * tag.length)
    return true
  }
  return false
})

md.renderer.rules.math = function(tokens, idx) {
  let content = tokens[idx].content,
      tag = tokens[idx].tag

  if (!content) return

  try {
    return  katex.renderToString(content, {
              displayMode: (tag === '$' ? false : true)
            })
  } catch (e) {
    return '{ parse error: ' + content + ' }'
  }
}

// custom-comment
md.block.ruler.before('code', 'custom-comment', function(state, start, end, silent) {
  let pos = state.bMarks[start] + state.tShift[start],
      max = state.eMarks[start]
  if (pos >= max) return false
  if (state.src.startsWith('', pos + 4)
    if (end >= 0) {
      let content = state.src.slice(pos + 4, end).trim()
      state.tokens.push({
        type: 'custom',
        content: content
      })
      state.line = start + 1
      return true
    } else {
      return false
    }
  }
})

// Settings
atom.config.observe('atom-markdown-katex.breakOnSingleNewline', (breakOnSingleNewline)=> {
  md.set({breaks: breakOnSingleNewline})
})

//
// Inject line numbers for sync scroll. Notes:
//
// - We track only headings and paragraphs on first level. That's enougth.
// - Footnotes content causes jumps. Level limit filter it automatically.
//
// YIYI : 这里我不仅仅 map 了 level 0
md.renderer.rules.paragraph_open = function (tokens, idx) {
  let line = null
  if (tokens[idx].lines /*&& tokens[idx].level === 0*/) {
    line = tokens[idx].lines[0]
    return '
'
  }
  return '
'
}

// Build offsets for each line (lines can be wrapped)
// That's a bit dirty to process each line everytime, but ok for demo.
// Optimizations are required only for big texts.
function buildScrollMap(markdownPreview) {
  var i, offset, nonEmptyList, pos, a, b, lineHeightMap, linesCount, acc, _scrollMap

  let editor = markdownPreview.editor
  let markdownHtmlView = markdownPreview.htmlView
  let lines = editor.getBuffer().getLines()

  // offset = $('.result-html').scrollTop() - $('.result-html').offset().top;
  _scrollMap = []
  nonEmptyList = []
  lineHeightMap = []

  acc = 0;
  lines.forEach(function(str, n) {
    lineHeightMap.push(editor.screenRowForBufferRow(n))
  })
  linesCount = editor.getScreenLineCount()

  for (let i = 0; i < linesCount; i++) { _scrollMap.push(-1); }

  nonEmptyList.push(0)
  _scrollMap[0] = 0

  // 把有标记 data-line 的 element 的 offsetTop 记录到 _scrollMap
  let lineElements = markdownHtmlView.querySelectorAll ('.line')
  for (let i = 0; i < lineElements.length; i++) {
    let el = lineElements[i],
        t = el.getAttribute('data-line')
    if (t === '') { return; }
    t = lineHeightMap[Number(t)] // get screen buffer row
    if (t !== 0) { nonEmptyList.push(t) }
    _scrollMap[t] = Math.round(el.offsetTop)
  }

  nonEmptyList.push(linesCount)
  _scrollMap[linesCount] = markdownHtmlView.scrollHeight;

  pos = 0
  for (i = 1; i < linesCount; i++) {
    if (_scrollMap[i] !== -1) {
      pos++
      continue
    }

    a = nonEmptyList[pos]
    b = nonEmptyList[pos + 1]
    _scrollMap[i] = Math.round((_scrollMap[b] * (i - a) + _scrollMap[a] * (b - i)) / (b - a))
  }

  return _scrollMap  // scrollMap's length == screenLineCount
}

// resolve image path and pre code block...
function resolveImagePathAndCodeBlock(html, directoryPath) {
  if (!directoryPath) return

  let $ = cheerio.load(html)
  $('img').each(function(i, imgElement) {
    let img = $(imgElement)
    let src = img.attr('src')
    if (src &&
      (!(src.startsWith('http://') ||
        src.startsWith('https://') ||
        src.startsWith('atom://')  ||
        src.startsWith('file://'))) &&
      (src.startsWith('./') ||
        src.startsWith('../') ||
        src[0] !== '/')) {
      img.attr('src', path.resolve(directoryPath,  src))
    }
  })

  $('pre').each(function(i, preElement) {
    let codeBlock = $(preElement).children().first()
    let lang = 'text'
    if (codeBlock.attr('class')) {
      lang = codeBlock.attr('class').replace(/^language-/, '') || 'text'
    }

    let highlighter = new Highlights({registry: atom.grammars}),
        html = highlighter.highlightSync({
          fileContents: codeBlock.text(),
          scopeName: scopeForLanguageName(lang),
        })

    let highlightedBlock = $(html)

    highlightedBlock.removeClass('editor').addClass('lang-' + lang)

    $(preElement).replaceWith(highlightedBlock)
  })

  return $.html()
}

// parse markdown content to html
function parseMD(markdownPreview) {
  let editor = markdownPreview.editor
  let rootDirectoryPath = markdownPreview.rootDirectoryPath

  let inputString = markdownPreview.textContent
  let headings = []

  // replace math expression
  let useKaTeX = atom.config.get('atom-markdown-katex.useKaTeX')

  let tocNeedUpdate = false
  let tocTable = {} // eliminate repeated slug
  let tocEnabled = false

  // overwrite remark heading parse function
  md.renderer.rules.heading_open = (tokens, idx)=> {
    let line = null,
        id = null

    if (tokens[idx + 1] && tokens[idx + 1].content) {
      id = uslug(tokens[idx + 1].content)
      if (tocTable[id] >= 0) {
        tocTable[id] += 1
        id = id + '-' + tocTable[id]
      } else {
        tocTable[id] = 0
      }
      if (!tocNeedUpdate) {
        headings.push(tokens[idx + 1].content)
      }
    }

    if (tokens[idx].lines /*&& tokens[idx].level === 0*/) {
      line = tokens[idx].lines[0]
      return `<h${tokens[idx].hLevel} class="line" data-line="${line}" ${id ? `id="${id}"` : ''}>`
    }
    return `<h${tokens[idx].hLevel} ${id ? `id="${id}"` : ''}>`
  }

  //
  md.renderer.rules.custom = (tokens, idx)=> {
    let content = tokens[idx].content

    if (content === 'pagebreak' || content === 'newpage') {
      return '

'
    } else if (content === 'toc') {
      tocEnabled = true
    }
    return ''
  }

  let html = md.render(inputString)

  // check toc update
  if (tocEnabled) {
    let oldHeadingsLength = markdownPreview.headings.length
    let newHeadingsLength = headings.length
    if (oldHeadingsLength !== newHeadingsLength) {
      markdownPreview.headings = headings
      tocNeedUpdate = true
    } else {
      for (let i = 0; i < headings.length; i++) {
        if (markdownPreview.headings[i] !== headings[i]) {
          markdownPreview.headings = headings
          tocNeedUpdate = true
          break
        }
      }
    }

    if (tocNeedUpdate) {
      console.log('toc update')
      let cursorPos = editor.getCursorScreenPosition()

      let replaced = toc.insert(inputString, {slugify: uslug})

      tocTable = {} // reset tocTable before md
      tocEnabled = false
      html = md.render(replaced)
      markdownPreview.editor.setText(replaced)

      markdownPreview.parseDelay = Date.now() + 500 // prevent render again
      markdownPreview.editorScrollDelay = Date.now() + 500
      markdownPreview.previewScrollDelay = Date.now() + 500

      // set cursor position
      if (newHeadingsLength > oldHeadingsLength) {
        editor.setCursorScreenPosition([cursorPos.row + 1, cursorPos.column])
      } else if (newHeadingsLength < oldHeadingsLength) {
        editor.setCursorScreenPosition([cursorPos.row - 1, cursorPos.column])
      } else {
        editor.setCursorScreenPosition([cursorPos.row, cursorPos.column])
      }
    }
  } else {
    markdownPreview.headings = headings
  }

  return resolveImagePathAndCodeBlock(html, rootDirectoryPath)
}

module.exports = {
  parseMD,
  buildScrollMap
}
<|endoftext|>


<commit_before>/*
 *  Ni - a minimalistic Node framework that makes setting up an MVC project
 *  a breeze. Inspired by CodeIgniter.
 *
 *  Since Ni is fully contained in a Node module, you can use it with Connect
 *  and your own choice of template engine, database, static file server, etc.
 *  No frills, no restrictions, no worries.
 */

/*
 *  Module dependencies
 */

var fs = require('fs'),
    Step = require('step');

/*
 *  Ni object
 */

var Ni = function() {
    var Ni = this;

    /*
     *  Registry for custom configuration variables.
     */

    this.config = {};

    this.automatic_views = false;

    /*
     *  Tells Ni what root directory to look into.
     *
     *  Call this function before calling boot.
     */

    this.setRoot = function(__root) {
        this.__root = __root;
    }

    /*
     *  Loads controllers, models, views, templates and helpers from the root
     *  directory, and makes them all available to the Ni object.
     */

    this.boot = function(callbackDone) {
        Step(
                function bootStrapEverything() {
                    bootStrapControllers(Ni.__root, this.parallel());
                    bootStrapModels(Ni.__root, this.parallel());
                    bootStrapViews(Ni.__root, this.parallel());
                    bootStrapLibraries(Ni.__root, this.parallel());
                },
                function buildNi(err, controllers, models, views, libraries) {
                    if (err) throw err;
                    Ni.controllers = controllers;
                    Ni.models = models;
                    Ni.views = views;
                    Ni.libraries = libraries;
                    callbackDone();
                }
            );
    }

    /*
     *  Provides router for Connect to automatically call the correct controller
     *  functions with the correct arguments based on the URL. If the controller
     *  or action is not found, it calls next() in the Connect list.
     *
     *  Add this to your Connect call as: Ni.router
     */

    this.router = function(req, res, next) {
        parsedUrl = require('url').parse(req.url, true);
        pathArr = parsedUrl.pathname.split('/');
        res.Ni = { view: Ni.automatic_views };

        var controller;
        if (pathArr[1]) {
            controller = Ni.controllers[pathArr[1]];
            if (controller) {
                res.Ni.controller = controller;
                var fn;
                if (pathArr[2]) {
                    res.Ni.action = pathArr[2];
                    fn = controller[pathArr[2]];
                }
                else {
                    res.Ni.action = 'index';
                    fn = controller.index;
                }

                if (fn) {
                    var args = pathArr.slice(3);
                    args.unshift(next);
                    args.unshift(res);
                    args.unshift(req);
                    fn.apply(null, args);
                }
                else {
                    res.Ni.controller = undefined;
                    res.Ni.action = undefined;
                    next();
                }
            }
            else {
                next();
            }
        }
        else {
            controller = Ni.controllers.home;
            if (controller && controller.index) {
                res.Ni.controller = 'home';
                res.Ni.action = 'index';
                controller.index(req, res, next);
            }
            else {
                next();
            }
        }
    }

    this.view = function (renderer) {
      if (typeof(renderer) === 'function') {
        Ni.automatic_views = true;
        return function (req, res, next) {
          if (res.Ni.view && typeof(res.Ni.action) !== 'undefined') {
            renderer(req, res, next, res.Ni.controller + '/' + res.Ni.action);
          } else {
            next();
          }
        }
      } else {
        throw new Error('Ni.view expects a function as the first argument');
      }
    }
};

/* Helper functions
*******************************************************************************/

/*
 *  Loads controllers.
 */

function bootStrapControllers(__root, callback) {
    bootStrapModules(__root + '/controllers', function(err, controllers) {
        if (err) throw err;
        callback(null, controllers);
    });
}

/*
 *  Loads models.
 */

function bootStrapModels(__root, callback) {
    bootStrapModules(__root + '/models', function(err, models) {
        if (err) throw err;
        callback(null, models);
    });
}

/*
 *  Loads views.
 */

function bootStrapViews(__root, callback) {
    var __views = __root + '/views';
    var views = {};

    loadFilesFromDir(__views, function(dir, file, name) {
        var view = {};
        view.path = dir + '/' + file;
        fs.readFile(view.path, 'utf-8', function(err, data) {
            view.template = data;
            views[name] = view;
        });
    }, function(err) {
        if (err) throw err;
        callback(null, views);
    });
}

/*
 *  Loads libraries.
 */

function bootStrapLibraries(__root, callback) {
    bootStrapModules(__root + '/libraries', function(err, libraries) {
        if (err) throw err;
        callback(null, libraries);
    });
}

/*
 *  Loads anything that is represented as a Node.js module.
 *  This includes controllers, models, libraries and helpers.
 */

function bootStrapModules(__root, callback) {
    var __modules = __root;
    var modules = {};

    loadFilesFromDir(__modules, function(dir, file, name) {
        var name = file.split('.')[0];
        modules[name] = require(dir + '/' + file);
    }, function(err) {
        if (err) callback(err);
        else callback(null, modules);
    });
}

/*
 *  Loads files from given directory and calls the given function on them.
 */

function loadFilesFromDir(dir, fnOnFile, callbackDone) {
    fs.readdir(dir, function(err, files){
        if (err || !files) callbackDone(null);
        else {
            Step(
                function readFiles() {
                    if (files.length > 0) {
                        var group = this.group();
                        files.forEach(function (file){
                            var name = file.split('.')[0];

                            fnOnFile(dir, file, name);
                            group()();
                        });
                    }
                    else {
                        return null;
                    }
                },
                function finish(err) {
                    if (err) callbackDone(err);
                    callbackDone(null);
                }
            );
        }
    });
}

/*
 *  Exports the Ni object to Node.js.
 */

module.exports = new Ni();<commit_msg>Fix controller name<commit_after>/*
 *  Ni - a minimalistic Node framework that makes setting up an MVC project
 *  a breeze. Inspired by CodeIgniter.
 *
 *  Since Ni is fully contained in a Node module, you can use it with Connect
 *  and your own choice of template engine, database, static file server, etc.
 *  No frills, no restrictions, no worries.
 */

/*
 *  Module dependencies
 */

var fs = require('fs'),
    Step = require('step');

/*
 *  Ni object
 */

var Ni = function() {
    var Ni = this;

    /*
     *  Registry for custom configuration variables.
     */

    this.config = {};

    this.automatic_views = false;

    /*
     *  Tells Ni what root directory to look into.
     *
     *  Call this function before calling boot.
     */

    this.setRoot = function(__root) {
        this.__root = __root;
    }

    /*
     *  Loads controllers, models, views, templates and helpers from the root
     *  directory, and makes them all available to the Ni object.
     */

    this.boot = function(callbackDone) {
        Step(
                function bootStrapEverything() {
                    bootStrapControllers(Ni.__root, this.parallel());
                    bootStrapModels(Ni.__root, this.parallel());
                    bootStrapViews(Ni.__root, this.parallel());
                    bootStrapLibraries(Ni.__root, this.parallel());
                },
                function buildNi(err, controllers, models, views, libraries) {
                    if (err) throw err;
                    Ni.controllers = controllers;
                    Ni.models = models;
                    Ni.views = views;
                    Ni.libraries = libraries;
                    callbackDone();
                }
            );
    }

    /*
     *  Provides router for Connect to automatically call the correct controller
     *  functions with the correct arguments based on the URL. If the controller
     *  or action is not found, it calls next() in the Connect list.
     *
     *  Add this to your Connect call as: Ni.router
     */

    this.router = function(req, res, next) {
        parsedUrl = require('url').parse(req.url, true);
        pathArr = parsedUrl.pathname.split('/');
        res.Ni = { view: Ni.automatic_views };

        var controller;
        if (pathArr[1]) {
            controller = Ni.controllers[pathArr[1]];
            if (controller) {
                res.Ni.controller = pathArr[1];
                var fn;
                if (pathArr[2]) {
                    res.Ni.action = pathArr[2];
                    fn = controller[pathArr[2]];
                }
                else {
                    res.Ni.action = 'index';
                    fn = controller.index;
                }

                if (fn) {
                    var args = pathArr.slice(3);
                    args.unshift(next);
                    args.unshift(res);
                    args.unshift(req);
                    fn.apply(null, args);
                }
                else {
                    res.Ni.controller = undefined;
                    res.Ni.action = undefined;
                    next();
                }
            }
            else {
                next();
            }
        }
        else {
            controller = Ni.controllers.home;
            if (controller && controller.index) {
                res.Ni.controller = 'home';
                res.Ni.action = 'index';
                controller.index(req, res, next);
            }
            else {
                next();
            }
        }
    }

    this.view = function (renderer) {
      if (typeof(renderer) === 'function') {
        Ni.automatic_views = true;
        return function (req, res, next) {
          if (res.Ni.view && typeof(res.Ni.action) !== 'undefined') {
            renderer(req, res, next, res.Ni.controller + '/' + res.Ni.action);
          } else {
            next();
          }
        }
      } else {
        throw new Error('Ni.view expects a function as the first argument');
      }
    }
};

/* Helper functions
*******************************************************************************/

/*
 *  Loads controllers.
 */

function bootStrapControllers(__root, callback) {
    bootStrapModules(__root + '/controllers', function(err, controllers) {
        if (err) throw err;
        callback(null, controllers);
    });
}

/*
 *  Loads models.
 */

function bootStrapModels(__root, callback) {
    bootStrapModules(__root + '/models', function(err, models) {
        if (err) throw err;
        callback(null, models);
    });
}

/*
 *  Loads views.
 */

function bootStrapViews(__root, callback) {
    var __views = __root + '/views';
    var views = {};

    loadFilesFromDir(__views, function(dir, file, name) {
        var view = {};
        view.path = dir + '/' + file;
        fs.readFile(view.path, 'utf-8', function(err, data) {
            view.template = data;
            views[name] = view;
        });
    }, function(err) {
        if (err) throw err;
        callback(null, views);
    });
}

/*
 *  Loads libraries.
 */

function bootStrapLibraries(__root, callback) {
    bootStrapModules(__root + '/libraries', function(err, libraries) {
        if (err) throw err;
        callback(null, libraries);
    });
}

/*
 *  Loads anything that is represented as a Node.js module.
 *  This includes controllers, models, libraries and helpers.
 */

function bootStrapModules(__root, callback) {
    var __modules = __root;
    var modules = {};

    loadFilesFromDir(__modules, function(dir, file, name) {
        var name = file.split('.')[0];
        modules[name] = require(dir + '/' + file);
    }, function(err) {
        if (err) callback(err);
        else callback(null, modules);
    });
}

/*
 *  Loads files from given directory and calls the given function on them.
 */

function loadFilesFromDir(dir, fnOnFile, callbackDone) {
    fs.readdir(dir, function(err, files){
        if (err || !files) callbackDone(null);
        else {
            Step(
                function readFiles() {
                    if (files.length > 0) {
                        var group = this.group();
                        files.forEach(function (file){
                            var name = file.split('.')[0];

                            fnOnFile(dir, file, name);
                            group()();
                        });
                    }
                    else {
                        return null;
                    }
                },
                function finish(err) {
                    if (err) callbackDone(err);
                    callbackDone(null);
                }
            );
        }
    });
}

/*
 *  Exports the Ni object to Node.js.
 */

module.exports = new Ni();<|endoftext|>


<commit_before>var fs = require('fs');
var path = require('path');
var _ = require('lodash');
var potentialFilenames = ['LICENSE', 'LICENSE.md', 'README', 'README.md', 'README.markdown', 'license.txt'];
var wellKnownLincenses = ['MIT','BSD','Apache License','Mozilla','LGPL','Affero','GPL','Eclipse','Artistic','DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE'];

var licenseFromString = function(str){
    for (var license of wellKnownLincenses) {
        if (str.indexOf(license) >= 0) {
            return license;
        }
    }
    return undefined
}

module.exports = {
    find: function(packagePath) {
        var lowwercaseNames = potentialFilenames.map(function(pfn) {
            return pfn.toLowerCase();
        });

        var licenses = [];
        var packagePath = packagePath || '.'
        var files = fs.readdirSync(packagePath).filter(function(f) {
            return fs.lstatSync(path.resolve(packagePath, f)).isFile();
        });

        for (var file of files) {
            if (lowwercaseNames.indexOf(file.toLowerCase()) >= 0) {
                var fileContent = fs.readFileSync(path.resolve(packagePath, file), 'utf8');
                var licenseName = licenseFromString(fileContent);
                if (typeof licenseName !== 'undefined') {
                    var content = 'todo';
                    if (file.toLowerCase().indexOf('license')>=0) {
                        content = fileContent;
                    }
                    licenses.push({
                        type: licenseName,
                        content: content
                    });
                }
            }
        }
        // if only one license is found remove array
        if (_.isArray(licenses) && licenses.length === 1) {
            licenses = licenses[0];
        }
        // remove duplicates
        if (_.isArray(licenses) && licenses.length > 1) {
            licenses = _.uniqWith(licenses, _.isEqual);
        }

        return licenses
    }
}
<commit_msg>review suggestion, be consistent of the data type of return value<commit_after>var fs = require('fs');
var path = require('path');
var _ = require('lodash');
var potentialFilenames = ['LICENSE', 'LICENSE.md', 'README', 'README.md', 'README.markdown', 'license.txt'];
var wellKnownLincenses = ['MIT','BSD','Apache License','Mozilla','LGPL','Affero','GPL','Eclipse','Artistic','DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE'];

var licenseFromString = function(str){
    for (var license of wellKnownLincenses) {
        if (str.indexOf(license) >= 0) {
            return license;
        }
    }
    return undefined
}

module.exports = {
    find: function(packagePath) {
        var lowwercaseNames = potentialFilenames.map(function(pfn) {
            return pfn.toLowerCase();
        });

        var licenses = [];
        var packagePath = packagePath || '.'
        var files = fs.readdirSync(packagePath).filter(function(f) {
            return fs.lstatSync(path.resolve(packagePath, f)).isFile();
        });

        for (var file of files) {
            if (lowwercaseNames.indexOf(file.toLowerCase()) >= 0) {
                var fileContent = fs.readFileSync(path.resolve(packagePath, file), 'utf8');
                var licenseName = licenseFromString(fileContent);
                if (typeof licenseName !== 'undefined') {
                    var content = 'todo';
                    if (file.toLowerCase().indexOf('license')>=0) {
                        content = fileContent;
                    }
                    licenses.push({
                        type: licenseName,
                        content: content
                    });
                }
            }
        }

        // remove duplicates
        if (_.isArray(licenses) && licenses.length > 1) {
            licenses = _.uniqWith(licenses, _.isEqual);
        }

        return licenses
    }
}
<|endoftext|>


<commit_before>"use strict";

var Readable = require('stream').Readable;

var QR = require('./qr-base').QR;
var png = require('./png');
var vector = require('./vector');

var fn_noop = function() {};

var BITMAP_OPTIONS = {
    parse_url: false,
    ec_level: 'M',
    size: 5,
    margin: 4,
    customize: null
};

var VECTOR_OPTIONS = {
    parse_url: false,
    ec_level: 'M',
    margin: 1,
    size: 0
};

function get_options(options, force_type) {
    if (typeof options === 'string') {
        options = { 'ec_level': options }
    } else {
        options = options || {};
    }
    var _options = {
        type: String(force_type || options.type || 'png').toLowerCase()
    };

    var defaults = _options.type == 'png' ? BITMAP_OPTIONS : VECTOR_OPTIONS;

    for (var k in defaults) {
        _options[k] = k in options ? options[k] : defaults[k];
    }

    return _options;
}

function qr_image(text, options) {
    options = get_options(options);

    var matrix = QR(text, options.ec_level, options.parse_url);
    var stream = new Readable();
    stream._read = fn_noop;

    switch (options.type) {
    case 'svg':
    case 'pdf':
    case 'eps':
        process.nextTick(function() {
            vector[options.type](matrix, stream, options.margin, options.size);
        });
        break;
    case 'svgpath':
        // deprecated, use svg_object method
        process.nextTick(function() {
            var obj = vector.svg_object(matrix, options.margin);
            stream.push(obj.path);
            stream.push(null);
        });
        break;
    case 'png':
    default:
        process.nextTick(function() {
            var bitmap = png.bitmap(matrix, options.size, options.margin);
            if (options.customize) {
                options.customize(bitmap);
            }
            png.PNG(bitmap, stream);
        });
    }

    return stream;
}

function qr_image_sync(text, options) {
    options = get_options(options);

    var matrix = QR(text, options.ec_level);
    var result;

    switch (options.type) {
    case 'svg':
    case 'pdf':
    case 'eps':
        var stream = [];
        vector[options.type](matrix, stream, options.margin, options.size);
        result = stream.filter(Boolean).join('');
        break;
    case 'png':
    default:
        throw new Error('Not implemented yet');
    }

    return result;
}

function svg_object(text, options) {
    options = get_options(options, 'svg');

    var matrix = QR(text, options.ec_level);
    return vector.svg_object(matrix, options.margin);
}

module.exports = {
    matrix: QR,
    image: qr_image,
    imageSync: qr_image_sync,
    svgObject: svg_object
};
<commit_msg>Support parse_url option in imageSync<commit_after>"use strict";

var Readable = require('stream').Readable;

var QR = require('./qr-base').QR;
var png = require('./png');
var vector = require('./vector');

var fn_noop = function() {};

var BITMAP_OPTIONS = {
    parse_url: false,
    ec_level: 'M',
    size: 5,
    margin: 4,
    customize: null
};

var VECTOR_OPTIONS = {
    parse_url: false,
    ec_level: 'M',
    margin: 1,
    size: 0
};

function get_options(options, force_type) {
    if (typeof options === 'string') {
        options = { 'ec_level': options }
    } else {
        options = options || {};
    }
    var _options = {
        type: String(force_type || options.type || 'png').toLowerCase()
    };

    var defaults = _options.type == 'png' ? BITMAP_OPTIONS : VECTOR_OPTIONS;

    for (var k in defaults) {
        _options[k] = k in options ? options[k] : defaults[k];
    }

    return _options;
}

function qr_image(text, options) {
    options = get_options(options);

    var matrix = QR(text, options.ec_level, options.parse_url);
    var stream = new Readable();
    stream._read = fn_noop;

    switch (options.type) {
    case 'svg':
    case 'pdf':
    case 'eps':
        process.nextTick(function() {
            vector[options.type](matrix, stream, options.margin, options.size);
        });
        break;
    case 'svgpath':
        // deprecated, use svg_object method
        process.nextTick(function() {
            var obj = vector.svg_object(matrix, options.margin);
            stream.push(obj.path);
            stream.push(null);
        });
        break;
    case 'png':
    default:
        process.nextTick(function() {
            var bitmap = png.bitmap(matrix, options.size, options.margin);
            if (options.customize) {
                options.customize(bitmap);
            }
            png.PNG(bitmap, stream);
        });
    }

    return stream;
}

function qr_image_sync(text, options) {
    options = get_options(options);

    var matrix = QR(text, options.ec_level, options.parse_url);
    var result;

    switch (options.type) {
    case 'svg':
    case 'pdf':
    case 'eps':
        var stream = [];
        vector[options.type](matrix, stream, options.margin, options.size);
        result = stream.filter(Boolean).join('');
        break;
    case 'png':
    default:
        throw new Error('Not implemented yet');
    }

    return result;
}

function svg_object(text, options) {
    options = get_options(options, 'svg');

    var matrix = QR(text, options.ec_level);
    return vector.svg_object(matrix, options.margin);
}

module.exports = {
    matrix: QR,
    image: qr_image,
    imageSync: qr_image_sync,
    svgObject: svg_object
};
<|endoftext|>


<commit_before>'use strict';

var Bluebird = require('./bluebird-fresh.js'),
    printRejectionReason = require('./bluebird-captured-trace-fresh.js').possiblyUnhandledRejection,
    _ = require('lodash');

// Load Request freshly - so that users can require an unaltered request instance!
var request = (function () {

    function clearCache() {
        _(require.cache).keys().forEach(function (key) {
            delete require.cache[key];
        });
    }

    var temp = _.assign({}, require.cache);
    clearCache();

    var freshRequest = require('request');

    clearCache();
    _.assign(require.cache, temp);

    return freshRequest;

})();

function ownCallback(err, httpResponse, body) {

    /* jshint validthis:true */
    var self = this;

    var origCallbackThrewException = false, thrownException;

    if (_.isFunction(self._rp_callbackOrig)) {
        try {
            self._rp_callbackOrig.apply(self, arguments);
        } catch (e) {
            origCallbackThrewException = true;
            thrownException = e;
        }
    }

    if (err) {
        self._rp_reject({
            error: err,
            options: self._rp_options,
            response: httpResponse
        });
    } else if (self._rp_options.simple && !(/^2/.test('' + httpResponse.statusCode))) {
        self._rp_reject({
            error: body,
            options: self._rp_options,
            response: httpResponse,
            statusCode: httpResponse.statusCode
        });
    } else {
        if (_.isFunction(self._rp_options.transform)) {
            try {
                self._rp_resolve(self._rp_options.transform(body, httpResponse));
            } catch (e) {
                self._rp_reject(e);
            }
        } else if (self._rp_options.resolveWithFullResponse) {
            self._rp_resolve(httpResponse);
        } else {
            self._rp_resolve(body);
        }
    }

    if (origCallbackThrewException) {
        throw thrownException;
    }

    // Mimic original behavior of errors emitted by request with no error listener registered
    if (err && _.isFunction(self._rp_callbackOrig) === false && self._rp_promise_in_use !== true && self.listeners('error').length === 1) {
        throw err;
    }

}

var originalInit = request.Request.prototype.init;

request.Request.prototype.init = function (options) {

    var self = this;

    // Init may be called again - currently in case of redirects
    if (_.isPlainObject(options) && self._callback === undefined && self._rp_promise === undefined) {

        self._rp_promise = new Bluebird(function (resolve, reject) {
            self._rp_resolve = resolve;
            self._rp_reject = reject;
        });
        self._rp_promise._rp_in_use = false;

        self._rp_callbackOrig = self.callback;
        self.callback = ownCallback;

        if (_.isString(options.method)) {
            options.method = options.method.toUpperCase();
        }

        self._rp_options = options;
        self._rp_options.simple = options.simple === false ? false : true;
        self._rp_options.resolveWithFullResponse = options.resolveWithFullResponse === true ? true : false;

    }

    return originalInit.apply(self, arguments);

};

function markPromiseInUse(requestInstance) {
    requestInstance._rp_promise_in_use = true;
    requestInstance._rp_promise._rp_in_use = true;
}

request.Request.prototype.then = function RP$then(onFulfilled, onRejected) {
    markPromiseInUse(this);
    return this._rp_promise.then.apply(this._rp_promise, arguments);
};

request.Request.prototype.catch = function RP$catch(onRejected) {
    markPromiseInUse(this);
    return this._rp_promise.catch.apply(this._rp_promise, arguments);
};

request.Request.prototype.finally = function RP$finally() {
    markPromiseInUse(this);
    return this._rp_promise.finally.apply(this._rp_promise);
};

request.Request.prototype.caught = request.Request.prototype.catch;

request.Request.prototype.lastly = request.Request.prototype.finally;

request.Request.prototype.promise = function RP$promise() {
    markPromiseInUse(this);
    return this._rp_promise;
};

Bluebird.onPossiblyUnhandledRejection(function (reason, promise) {
    // For whatever reason we don't see _rp_in_use here at all after then is called. --> We compare to false instead of true.
    if (promise._rp_in_use !== false) {
        printRejectionReason(reason);
    }
    // else: The user did not call .then(...)
    // --> We need to assume that this request is processed with a callback or a pipe etc.
});

module.exports = request;
<commit_msg>Renamed inspectable functions<commit_after>'use strict';

var Bluebird = require('./bluebird-fresh.js'),
    printRejectionReason = require('./bluebird-captured-trace-fresh.js').possiblyUnhandledRejection,
    _ = require('lodash');

// Load Request freshly - so that users can require an unaltered request instance!
var request = (function () {

    function clearCache() {
        _(require.cache).keys().forEach(function (key) {
            delete require.cache[key];
        });
    }

    var temp = _.assign({}, require.cache);
    clearCache();

    var freshRequest = require('request');

    clearCache();
    _.assign(require.cache, temp);

    return freshRequest;

})();

function RP$callback(err, httpResponse, body) {

    /* jshint validthis:true */
    var self = this;

    var origCallbackThrewException = false, thrownException;

    if (_.isFunction(self._rp_callbackOrig)) {
        try {
            self._rp_callbackOrig.apply(self, arguments);
        } catch (e) {
            origCallbackThrewException = true;
            thrownException = e;
        }
    }

    if (err) {
        self._rp_reject({
            error: err,
            options: self._rp_options,
            response: httpResponse
        });
    } else if (self._rp_options.simple && !(/^2/.test('' + httpResponse.statusCode))) {
        self._rp_reject({
            error: body,
            options: self._rp_options,
            response: httpResponse,
            statusCode: httpResponse.statusCode
        });
    } else {
        if (_.isFunction(self._rp_options.transform)) {
            try {
                self._rp_resolve(self._rp_options.transform(body, httpResponse));
            } catch (e) {
                self._rp_reject(e);
            }
        } else if (self._rp_options.resolveWithFullResponse) {
            self._rp_resolve(httpResponse);
        } else {
            self._rp_resolve(body);
        }
    }

    if (origCallbackThrewException) {
        throw thrownException;
    }

    // Mimic original behavior of errors emitted by request with no error listener registered
    if (err && _.isFunction(self._rp_callbackOrig) === false && self._rp_promise_in_use !== true && self.listeners('error').length === 1) {
        throw err;
    }

}

var originalInit = request.Request.prototype.init;

request.Request.prototype.init = function RP$initInterceptor(options) {

    var self = this;

    // Init may be called again - currently in case of redirects
    if (_.isPlainObject(options) && self._callback === undefined && self._rp_promise === undefined) {

        self._rp_promise = new Bluebird(function (resolve, reject) {
            self._rp_resolve = resolve;
            self._rp_reject = reject;
        });
        self._rp_promise._rp_in_use = false;

        self._rp_callbackOrig = self.callback;
        self.callback = RP$callback;

        if (_.isString(options.method)) {
            options.method = options.method.toUpperCase();
        }

        self._rp_options = options;
        self._rp_options.simple = options.simple === false ? false : true;
        self._rp_options.resolveWithFullResponse = options.resolveWithFullResponse === true ? true : false;

    }

    return originalInit.apply(self, arguments);

};

function markPromiseInUse(requestInstance) {
    requestInstance._rp_promise_in_use = true;
    requestInstance._rp_promise._rp_in_use = true;
}

request.Request.prototype.then = function RP$then(onFulfilled, onRejected) {
    markPromiseInUse(this);
    return this._rp_promise.then.apply(this._rp_promise, arguments);
};

request.Request.prototype.catch = function RP$catch(onRejected) {
    markPromiseInUse(this);
    return this._rp_promise.catch.apply(this._rp_promise, arguments);
};

request.Request.prototype.finally = function RP$finally() {
    markPromiseInUse(this);
    return this._rp_promise.finally.apply(this._rp_promise);
};

request.Request.prototype.caught = request.Request.prototype.catch;

request.Request.prototype.lastly = request.Request.prototype.finally;

request.Request.prototype.promise = function RP$promise() {
    markPromiseInUse(this);
    return this._rp_promise;
};

Bluebird.onPossiblyUnhandledRejection(function (reason, promise) {
    // For whatever reason we don't see _rp_in_use here at all after then is called. --> We compare to false instead of true.
    if (promise._rp_in_use !== false) {
        printRejectionReason(reason);
    }
    // else: The user did not call .then(...)
    // --> We need to assume that this request is processed with a callback or a pipe etc.
});

module.exports = request;
<|endoftext|>


<commit_before>/*!
 * Module dependencies.
 */

var Command = require('./util/command'),
    cordova = require('../cordova').cordova,
    cordovaLib = require('../cordova').lib,
    path = require('path'),
    util = require('util'),
    fs = require('fs'),
    crypto = require('crypto');

/*!
 * Command setup.
 */

module.exports = {
    create: function(phonegap) {
        return new CreateCommand(phonegap);
    }
};

function CreateCommand(phonegap) {
    return Command.apply(this, arguments);
}

util.inherits(CreateCommand, Command);

/**
 * Create a New App.
 *
 * Creates an project on the local filesystem.
 * This project is backwards compatible with Apache Cordova projects.
 *
 * Options:
 *
 *   - `options` {Object} is data required to create an app
 *     - `path` {String} is a directory path for the app.
 *     - `name` {String} is the application name (default: 'Hello World')
 *     - `id` {String} is the package name (default: 'com.phonegap.hello-world')
 *     - `config` {Object} is a JSON configuration object (default: {})
 *     - `link-to` {String} is a path to a project to link (default: undefined)
 *     - `copy-from` {String} is a path to a project to copy (default: undefined)
 *   - [`callback`] {Function} is triggered after creating the app.
 *     - `e` {Error} is null unless there is an error.
 *
 * Returns:
 *
 *   {PhoneGap} for chaining.
 */

CreateCommand.prototype.run = function(options, callback) {
    // require options
    if (!options) throw new Error('requires option parameter');
    if (!options.path) throw new Error('requires option.path parameter');

    // optional callback
    callback = callback || function() {};

    // validate options
    options.path = path.resolve(options.path.toString());
    options.name = options.name || 'Hello World';
    options.id = options.id || 'com.phonegap.helloworld';

    // validate options.config
    // ignore objects with no properties `{}` because this cleans up
    // the cordova-cli calls and prevents unexpected errors since thec
    // cordova-cli expects the config to always be the fourth argument.
    if (options.config && Object.keys(options.config).length > 0) {
        // the config argument is a JSON string, which means it must be properly
        // escaped for the command-line
        options.config = JSON.stringify(options.config).replace(/"/g, '\\"');
    }
    else {
        options.config = undefined;
    }

    // create app
    this.execute(options, callback);

    return this.phonegap;
};

/*!
 * Execute.
 */

CreateCommand.prototype.execute = function(options, callback) {
    var self = this,
        version = 'master',
        url = 'https://github.com/phonegap/phonegap-app-hello-world/archive/' + version + '.tar.gz';

    // use the phonegap hello world app template,
    // unless the user has specified a custom template.
    if (!declaresTemplate(options)) {
        cordova.config(options.path, {
            lib: {
                www: {
                    id: 'com.phonegap.hello-world',
                    version: version,
                    uri: url,
                    link: false
                }
            }
        });
    }

    // construct the cordova create command
    var cordovaCommand = 'cordova create';
    [ options.path, options.id, options.name, options.config ].forEach(function(value) {
        if (typeof value !== 'undefined') {
            cordovaCommand += ' "$value"'.replace('$value', value);
        }
    });

    // use --copy-from (or -src) if it existed in the original command
    if (options['copy-from']) {
        cordovaCommand += ' --copy-from="%s"'.replace('%s', options['copy-from']);
    }

    if (options['recipe']) {
        var url = require('../../recipes')[options['recipe']].url
        // FIXME add an error here if we don't find a recipe
        cordova.config(options.path, {
            lib: {
                www: {
                    id: crypto.createHash('sha1').update(url).digest("hex"),
                    version: version,
                    uri: url,
                    link: false
                }
            }
        });
    }

    // use --link-to if it existed in the original command
    if (options['link-to']) {
        cordovaCommand += ' --link-to="%s"'.replace('%s', options['link-to']);
    }

    // use cordova to create the project from the command-line
    self.phonegap.cordova({ cmd: cordovaCommand }, function(e) {
        if (e) {
            // do not emit this error because it'll be handled by
            // the `phonegap.cordova` function.
            callback(e);
            return;
        }

        // do not alter linked projects
        if (!options['link-to']) {
            var configXML = {
                projectPath: path.join(options.path, 'config.xml'),
                wwwPath: path.join(options.path, 'www', 'config.xml')
            };

            // move config.xml to root of project for legacy app templates
            if (fs.existsSync(configXML.wwwPath)) {
                fs.renameSync(configXML.wwwPath, configXML.projectPath);
            }

            // update config.xml with app info
            if (fs.existsSync(configXML.projectPath)) {
                var configParser = new cordovaLib.configparser(configXML.projectPath);
                configParser.setPackageName(options.id);
                configParser.setName(options.name);
                configParser.write();
            }
            else {
                self.phonegap.emit('warn', 'could not update ' + configXML.projectpath);
            }
        }

        callback(e);
    });
};

/*!
 * Check if project template declared in options.
 *
 * Helper method to do as the title states.
 */

function declaresTemplate(options) {
    // return true if a template is delcared in the options
    return (
        options['copy-from'] ||
        options['link-to'] ||
        options['recipe'] ||
        (
            options.config &&
            options.config.lib &&
            options.config.lib.www
        )
    );
}
<commit_msg>[#46] Update lib/phonegap/create.js to pass jshint.<commit_after>/*!
 * Module dependencies.
 */

var Command = require('./util/command'),
    cordova = require('../cordova').cordova,
    cordovaLib = require('../cordova').lib,
    crypto = require('crypto'),
    path = require('path'),
    util = require('util'),
    fs = require('fs');

/*!
 * Command setup.
 */

module.exports = {
    create: function(phonegap) {
        return new CreateCommand(phonegap);
    }
};

function CreateCommand(phonegap) {
    return Command.apply(this, arguments);
}

util.inherits(CreateCommand, Command);

/**
 * Create a New App.
 *
 * Creates an project on the local filesystem.
 * This project is backwards compatible with Apache Cordova projects.
 *
 * Options:
 *
 *   - `options` {Object} is data required to create an app
 *     - `path` {String} is a directory path for the app.
 *     - `name` {String} is the application name (default: 'Hello World')
 *     - `id` {String} is the package name (default: 'com.phonegap.hello-world')
 *     - `config` {Object} is a JSON configuration object (default: {})
 *     - `link-to` {String} is a path to a project to link (default: undefined)
 *     - `copy-from` {String} is a path to a project to copy (default: undefined)
 *   - [`callback`] {Function} is triggered after creating the app.
 *     - `e` {Error} is null unless there is an error.
 *
 * Returns:
 *
 *   {PhoneGap} for chaining.
 */

CreateCommand.prototype.run = function(options, callback) {
    // require options
    if (!options) throw new Error('requires option parameter');
    if (!options.path) throw new Error('requires option.path parameter');

    // optional callback
    callback = callback || function() {};

    // validate options
    options.path = path.resolve(options.path.toString());
    options.name = options.name || 'Hello World';
    options.id = options.id || 'com.phonegap.helloworld';

    // validate options.config
    // ignore objects with no properties `{}` because this cleans up
    // the cordova-cli calls and prevents unexpected errors since thec
    // cordova-cli expects the config to always be the fourth argument.
    if (options.config && Object.keys(options.config).length > 0) {
        // the config argument is a JSON string, which means it must be properly
        // escaped for the command-line
        options.config = JSON.stringify(options.config).replace(/"/g, '\\"');
    }
    else {
        options.config = undefined;
    }

    // create app
    this.execute(options, callback);

    return this.phonegap;
};

/*!
 * Execute.
 */

CreateCommand.prototype.execute = function(options, callback) {
    var self = this,
        version = 'master',
        url = 'https://github.com/phonegap/phonegap-app-hello-world/archive/' + version + '.tar.gz';

    // use the phonegap hello world app template,
    // unless the user has specified a custom template.
    if (!declaresTemplate(options)) {
        cordova.config(options.path, {
            lib: {
                www: {
                    id: 'com.phonegap.hello-world',
                    version: version,
                    uri: url,
                    link: false
                }
            }
        });
    }

    // construct the cordova create command
    var cordovaCommand = 'cordova create';
    [ options.path, options.id, options.name, options.config ].forEach(function(value) {
        if (typeof value !== 'undefined') {
            cordovaCommand += ' "$value"'.replace('$value', value);
        }
    });

    // use --copy-from (or -src) if it existed in the original command
    if (options['copy-from']) {
        cordovaCommand += ' --copy-from="%s"'.replace('%s', options['copy-from']);
    }

    if (options.recipe) {
        var recipeUrl = require('../../recipes')[options.recipe].url;
        // FIXME add an error here if we don't find a recipe
        cordova.config(options.path, {
            lib: {
                www: {
                    id: crypto.createHash('sha1').update(recipeUrl).digest("hex"),
                    version: version,
                    uri: recipeUrl,
                    link: false
                }
            }
        });
    }

    // use --link-to if it existed in the original command
    if (options['link-to']) {
        cordovaCommand += ' --link-to="%s"'.replace('%s', options['link-to']);
    }

    // use cordova to create the project from the command-line
    self.phonegap.cordova({ cmd: cordovaCommand }, function(e) {
        if (e) {
            // do not emit this error because it'll be handled by
            // the `phonegap.cordova` function.
            callback(e);
            return;
        }

        // do not alter linked projects
        if (!options['link-to']) {
            var configXML = {
                projectPath: path.join(options.path, 'config.xml'),
                wwwPath: path.join(options.path, 'www', 'config.xml')
            };

            // move config.xml to root of project for legacy app templates
            if (fs.existsSync(configXML.wwwPath)) {
                fs.renameSync(configXML.wwwPath, configXML.projectPath);
            }

            // update config.xml with app info
            if (fs.existsSync(configXML.projectPath)) {
                var configParser = new cordovaLib.configparser(configXML.projectPath);
                configParser.setPackageName(options.id);
                configParser.setName(options.name);
                configParser.write();
            }
            else {
                self.phonegap.emit('warn', 'could not update ' + configXML.projectpath);
            }
        }

        callback(e);
    });
};

/*!
 * Check if project template declared in options.
 *
 * Helper method to do as the title states.
 */

function declaresTemplate(options) {
    // return true if a template is delcared in the options
    return (
        options['copy-from'] ||
        options['link-to'] ||
        options['recipe'] ||
        (
            options.config &&
            options.config.lib &&
            options.config.lib.www
        )
    );
}
<|endoftext|>


<commit_before>//unique function taken from http://snipplr.com/view/45323/remove-duplicate-values-from-array/
//probably should be refactored. this only works with small arrays
var unique = function(origArr) {
    var newArr = [],
        origLen = origArr.length,
        found,
        x, y;

    for ( x = 0; x < origLen; x++ ) {
        found = undefined;
        for ( y = 0; y < newArr.length; y++ ) {
            if ( origArr[x] === newArr[y] ) {
              found = true;
              break;
            }
        }
        if ( !found) newArr.push( origArr[x] );
    }
   return newArr;
};

//to avoid regex, simplify things here with endsWith
function endsWith(str, suffix) {
    return str.indexOf(suffix, str.length - suffix.length) !== -1;
}

//given a phrase that contains a tag, replace all occurences one-by-one
var replaceTag = function(phrase, tag, replacements){
  var result = "";
  var firstTag = false;
  var lastTag = false;
  var i = 0;
  var replacementIndex = null;

  if (!replacements || replacements.length===0) return phrase.replace(tag, "");

  //first deal with potential tags at the beginning and end of the phrase.
  if (phrase.indexOf(tag)===0){
    result = phrase.replace(tag, replacements[0]);
    firstTag = true;
  }

  //end of phrase check
  if (endsWith(phrase, tag)){
    lastTag = true;
  }

  //split the input string by tag delimiters
  // "some %s thing %s we % say" becomes ['some ',' thing ', ' we ', ' say']
  var resultArray = phrase.split(tag);

  //join the array back with replacement values
  //assuming replacement values contains ['value1', 'value2']
  //we would expect the end string to be
  //"some value1 thing value2 we value1 say" given the fact that we wrap around the replacements by design
  for (i; i < resultArray.length-1; i++){
    replacementIndex = i%(replacements.length);

    result += resultArray[i] + replacements[replacementIndex];
  }

  //lastly, if there was a tag at the end, let's append it
  if (lastTag){
    replacementIndex = (i+1)%(replacements.length);
    result += replacements[replacementIndex];
  }

  //boom! we got our tag of a specific type replaced
  return result;
};

module.exports = {
  replace : function(phrase, replacements){
    //replacements is a JSON object with defined replacements for each %tag
    if (!phrase) return null;

    var newPhrase = phrase;

    //first figure out what tokens we need to replace
    //keep them in an array. match anything with a % prefix
    var tokens = phrase.match(/%[a-z]*/g);
    //in case of a clean, non-tagged phrase - return it.
    if (!tokens){
      return phrase;
    }
    //if we have a tagged phrase, split tags into tokens
    var replacementTokens = tokens.toString().split(',');

    //let's get a unique list of tokens
    var uniqueTokens = unique(replacementTokens);

    //then itterate through the array and do the replacements for each token type
    for (var index in uniqueTokens){
      //trim % from the tag so we can select it from JSON
      var tag = uniqueTokens[index].replace("%", "");
      newPhrase = replaceTag(newPhrase, "%"+tag, replacements[tag]);
    }

    return newPhrase;
  }
};
<commit_msg>#9 - adding last array element. 7/13 tests now passing<commit_after>//unique function taken from http://snipplr.com/view/45323/remove-duplicate-values-from-array/
//probably should be refactored. this only works with small arrays
var unique = function(origArr) {
    var newArr = [],
        origLen = origArr.length,
        found,
        x, y;

    for ( x = 0; x < origLen; x++ ) {
        found = undefined;
        for ( y = 0; y < newArr.length; y++ ) {
            if ( origArr[x] === newArr[y] ) {
              found = true;
              break;
            }
        }
        if ( !found) newArr.push( origArr[x] );
    }
   return newArr;
};

//to avoid regex, simplify things here with endsWith
function endsWith(str, suffix) {
    return str.indexOf(suffix, str.length - suffix.length) !== -1;
}

//given a phrase that contains a tag, replace all occurences one-by-one
var replaceTag = function(phrase, tag, replacements){
  var result = "";
  var firstTag = false;
  var lastTag = false;
  var i = 0;
  var replacementIndex = null;

  if (!replacements || replacements.length===0) return phrase.replace(tag, "");

  //first deal with potential tags at the beginning and end of the phrase.
  if (phrase.indexOf(tag)===0){
    result = phrase.replace(tag, replacements[0]);
    firstTag = true;
  }

  //end of phrase check
  if (endsWith(phrase, tag)){
    lastTag = true;
  }

  //split the input string by tag delimiters
  // "some %s thing %s we % say" becomes ['some ',' thing ', ' we ', ' say']
  var resultArray = phrase.split(tag);

  //join the array back with replacement values
  //assuming replacement values contains ['value1', 'value2']
  //we would expect the end string to be
  //"some value1 thing value2 we value1 say" given the fact that we wrap around the replacements by design
  for (i; i < resultArray.length-1; i++){
    replacementIndex = i%(replacements.length);

    result += resultArray[i] + replacements[replacementIndex];
  }
  //don't forget to add the last word
  result += resultArray[i];

  //lastly, if there was a tag at the end, let's append it
  if (lastTag){
    replacementIndex = (i+1)%(replacements.length);
    result += replacements[replacementIndex];
  }

  //boom! we got our tag of a specific type replaced
  return result;
};

module.exports = {
  replace : function(phrase, replacements){
    //replacements is a JSON object with defined replacements for each %tag
    if (!phrase) return null;

    var newPhrase = phrase;

    //first figure out what tokens we need to replace
    //keep them in an array. match anything with a % prefix
    var tokens = phrase.match(/%[a-z]*/g);
    //in case of a clean, non-tagged phrase - return it.
    if (!tokens){
      return phrase;
    }
    //if we have a tagged phrase, split tags into tokens
    var replacementTokens = tokens.toString().split(',');

    //let's get a unique list of tokens
    var uniqueTokens = unique(replacementTokens);

    //then itterate through the array and do the replacements for each token type
    for (var index in uniqueTokens){
      //trim % from the tag so we can select it from JSON
      var tag = uniqueTokens[index].replace("%", "");
      newPhrase = replaceTag(newPhrase, "%"+tag, replacements[tag]);
    }

    return newPhrase;
  }
};
<|endoftext|>


<commit_before>/*
 * Licensed to Cloudkick, Inc ('Cloudkick') under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * Cloudkick licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

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

var async = require('async');
var sprintf = require('sprintf').sprintf;
var express = require('express');

var config = require('util/config');
var log = require('util/log');
var fsUtil = require('util/fs');
var misc = require('util/misc');
var Errorf = misc.Errorf;

var constants = require('constants');
var httpServer = require('services/http').instance;
var pluginConstants = require('plugins/constants');
var pluginsHttpUtils = require('plugins/http');
var pluginServicesUtils = require('plugins/services');

/**
 * Plugin manager.
 * @constructor
 */
function PluginManager() {
  /* Store pluginName to plugin manifest mappings */
  this._availablePlugins = {};

  /* Store plugin name to plugin settings mappings */
  this._enabledPlugins = {};
}

/**
 * Return name of all the the available plugins (all the plugins which are located
 *  in the plugins directory).
 * @param {Function} callback Callback called with (err, availablePlugins)
 */
PluginManager.prototype.getAvailablePlugins = function(callback) {
  var self = this;
  var pluginsDir = config.get()['plugins']['root'];

  async.waterfall([
    function listPluginFiles(callback) {
      fs.readdir(pluginsDir, function(err, files) {
        if (err && err.errno !== constants.ENOENT) {
          callback(err);
          return;
        }

        files = files || [];
        callback(null, files);
      });
    },

    function readPluginsManifest(files, callback) {
      async.forEach(files, function(file, callback) {
        var pluginPath = path.join(pluginsDir, file);
        var pluginName = file;

        if (self._availablePlugins.hasOwnProperty(file)) {
          // Manifest for this file is already cached
          callback();
          return;
        }

        fs.stats(pluginPath, function(err, stats) {
          if (err) {
            log.error('Error while stating plugin file / directory: %s',
                      err.message);
            callback();
            return;
         }

         if (!stats.isDirectory()) {
           // Plugin must be a directory
           callback();
           return;
         }

         self._readPluginManifest(pluginPath, function(err, manifestObj) {
           if (err) {
             log.error('Error while reading plugin manifest: %s', err.message);
             callback();
             return;
           }

           self._availablePlugins[pluginName] = manifestObj;
           callback();
          });
        });

        self._readPluginManifest(pluginPath, function(err, manifestObj) {
          if (err) {
            log.error('Error while reading plugin manifest: %s', err.message);
            callback();
            return;
          }

          self._availablePlugins[pluginName] = manifestObj;
        });
      }, callback);
    }
  ],

  function(err) {
    callback(err, (err) ? null : self._availablePlugins);
  });
};

/**
 * Return an object with the enabled plugins and their settings.
 * @param {Function} callback Callback called with (err, enabledPlugins)
 */
PluginManager.prototype.getEnabledPlugins = function(callback) {
  callback(null, config.get()['plugins']['enabled']);
};

/**
 * Read "plugins" part from the settings file and load all the enabled plugins.
 *
 * @param {Function} callback Callback called when the initialization completes.
 */
PluginManager.prototype._initialize = function(callback) {
  var self = this;
  var pluginName, pluginSettings;
  var confPlugins = config.get()['plugins'] || {};
  var enabledPlugins = confPlugins['enabled'];
  var enabledPluginNames = Object.keys(enabledPlugins);

  if (!enabledPlugins) {
    log.info('No plugins have been enabled');
    return;
  }

  async.forEach(enabledPluginNames, function(pluginName, callback) {
    var pluginSettings = enabledPlugins[pluginName];
    self._validatePluginSettings(pluginName, pluginSettings, function onValidated(err) {
      if (err) {
        log.error('Failed to validate plugin "%s" settings: %s', pluginName, err);
        callback();
        return;
      }

      // Load and enable the plugin
      log.info(sprintf('Enabling plugin "%s"', pluginName));
      self.enablePlugin(pluginName, callback);
    });
  }, callback);
};

/**
 * Check if the current version of the agent is supported by the plugin.
 *
 * @param {Object} pluginManifest Plugin manifest (parsed plugin package.json).
 * @return {Bool} true if the version is supported, false otherwise.
 */
PluginManager.prototype.isSupported = function(pluginManifest) {
  var agentVersionConstrain = pluginManifest.hasOwnProperty('agent_version');
  if (!agentVersionConstrain) {
    return true;
  }

  return semver.satisfies(sprintf('%s.%s.%s', version.MAJOR, version.MINOR,
                                  version.PATCH), agentVersionConstrain);
};
 * Validate plugin settings. Validation rules are defined by plugin author in
 * the plugin package.json file.
 *
 * @param {String} pluginName Plugin name.
 * @param {Object} pluginSettings Plugin settings.
 */
PluginManager.prototype.validatePluginSettings = function(pluginName,
                                                          pluginSettings,
                                                          callback) {
  // @TODO: Cache available plugins in this._availablePlugins dictionary
  var self = this;
  var pluginPath = path.join(config.get()['plugins']['root'], pluginName);

  async.waterfall([
    function readPluginManifest(callback) {
      self._readPluginManifest(pluginPath, callback);
    },

    function validatePluginSettings(manifest, callback) {
      var rules = manifest['settings'];
      // @TODO: Use validators from manifest/...
      callback();
    }
  ],

  function(err) {
    callback(err);
  });
};

/**
 * Read plugin manifest file.
 *
 * @param {String} pluginPath Path to the plugin directory.
 * @param {Function} callback Callback called with (err, data)
 */
PluginManager.prototype._readPluginManifest = function(pluginPath, callback) {
  var manifestPath = path.join(pluginPath, pluginConstants.PLUGIN_MANIFEST_NAME);
  fsUtil.jsonFile(manifestPath, callback);
};

/*
 * Enable a plugin and does the following;
 * - Register plugin HTTP endpoints
 * - Register and load plugin services
 * - Register plugin job manager
 * @param {String} pluginName Plugin name.
 * @param {Function} callback Callback called with (err)
 */
PluginManager.prototype.enablePlugin = function(pluginName, callback) {
  var self = this;
  var pluginEndpointPaths = [], pluginServices = [];
  var ops = [];

  var pluginPath = path.join(config.get()['plugins']['root'], pluginName);

  if (misc.inArray(pluginName, Object.keys(this._enabledPlugins))) {
    callback(new Errorf('Plugin "%s" is already enabled'));
    return;
  }

  // Register HTTP endpoints
  function registerHttpEndpoints(endpointRoutes, callback) {
    async.forEach(endpointRoutes, function(endpointRoute, callback) {
      var registeredPaths = self._registerPluginEndpoints(endpointRoute, callback);
      pluginEndpointPaths = pluginEndpointPaths.concat(registeredPaths);
    }, callback);
  }

  // Register and start services
  function registerAndStartServices(services, callback) {
    async.forEach(services, function(service, callback) {
      // pluginServices.push(service.name);
      service.start();
      callback();
    }, callback);
  }

  // Discover all the resources (endpoints, services, jobs) and decide what needs
  // to be done
  async.paralell([
    function discoverHttpEndpoints(callback) {
      pluginsHttpUtils.discoverEndpoints(pluginPath, function onDiscover(err, routes) {
        if (!err && routes) {
          ops.push(async.apply(registerHttpEndpoints, routes));
        }

        callback();
      });
    },

    function discoverServices(callback) {
      pluginServicesUtils.discoverServices(pluginPath, function onDiscover(err, services) {
        if (!err && services) {
          ops.push(async.apply(registerAndStartServices, services));
        }

        callback();
      });
    }

    // @TODO: Discover and register jobs
  ],

  function(err) {
    async.parallel(ops, callback);
  });

  this._enabledPlugins[pluginName] = {
    'endpoints': pluginEndpointPaths, // Store full paths of the plugin endpoints
    'services': pluginServices, // Store names of the plugin services
    'jobs': [] // Store names of the plugin jobs
  };
};

/**
 * Register plugin HTTP endpoint handler with the Cast HTTP server.
 *
 * @param {Array} endpointRoutes Routes array (method, path, middleware, ..., handler)
 * @return {Array} All the paths which have been successfully registered.
 */
PluginManager.prototype._registerPluginEndpoints = function(endpointRoutes) {
  var i, len, route, routeArgsLen, method, path, fullPath, middleware, handler;
  var registerMethod, args;
  var registeredPaths = [];

  for (i = 0, len = endpointRoutes.length; i < len; i++) {
    route = endpointRoutes[i];
    routeArgsLen = route.length;

    if (routeArgsLen < 3) {
      // Invalid route, should we throw?
      continue;
    }

    method = route[0].toLowerCase();
    path = route[1];
    fullPath = sprintf('%s/%s', pluginConstants.HTTP_ENDPOINT_PREFIX, path);

    // Actually register it
    args = [];
    if (routeArgsLen === 3) {
      // No middleware
      middleware = [];
    }
    else {
      middleware = route.splice(2, routeArgsLen - 1);
    }

    handler = route[3];
    registerMethod = httpServer[method];

    if (middleware) {
      args.push(path);
      args = args.concat(middleware);
      args.push(handler);
    }
    else {
      args = [ path, handler ];
    }

    // @TODO: Handle possible error
    registerMethod.apply(httpServer, args);
    registeredPaths.push(fullPath);
  }

  return registeredPaths;
};

/**
 * Disable a plugin.
 *
 * @param {String} pluginName Plugin name.
 * @param {Function} callback Callback called with ()
 */
PluginManager.prototype.disablePlugin = function(pluginName, callback) {
  var i, len;
  if (!misc.inArray(pluginName, Object.keys(this._enabledPlugins))) {
    callback(new Errorf('Plugin "%s" is not enabled'));
    return;
  }

  var plugin = this._enabledPlugins[pluginName];

  async.series([
    // Remove all the plugin HTTP endpoints
    function removeHttpEndpoints(callback) {
      var endpoint;
      var endpoints = plugin['endpoints'];

      for (i = 0, len = endpoints.length; i < len; i++) {
        endpoint = endpoints[i];
        httpServer.remove(endpoint);
      }

      callback();
    },

    // Stop all the services
    function stopAndUnregisterServices(callback) {
      var service;
      var services = plugin['services'];
      for (i = 0, len = services.length; i < len; i++) {
        service = services[i];
        service.stop();
      }
    },

    // Remove jobs. Note all the pending jobs will be canceled.
    function stopAndRemoveJobs(callback) {
      // @TODO:
      callback();
    }
  ], callback);
};

exports.PluginManager = PluginManager;
<commit_msg>Add getPluginSettings and getPluginManifest method.<commit_after>/*
 * Licensed to Cloudkick, Inc ('Cloudkick') under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * Cloudkick licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

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

var async = require('async');
var sprintf = require('sprintf').sprintf;
var express = require('express');

var config = require('util/config');
var log = require('util/log');
var fsUtil = require('util/fs');
var misc = require('util/misc');
var Errorf = misc.Errorf;

var constants = require('constants');
var httpServer = require('services/http').instance;
var pluginConstants = require('plugins/constants');
var pluginsHttpUtils = require('plugins/http');
var pluginServicesUtils = require('plugins/services');

/**
 * Plugin manager.
 * @constructor
 */
function PluginManager() {
  /* Store pluginName to plugin manifest mappings */
  this._availablePlugins = {};

  /* Store plugin name to plugin settings mappings */
  this._enabledPlugins = {};
}

/**
 * Return name of all the the available plugins (all the plugins which are located
 *  in the plugins directory).
 * @param {Function} callback Callback called with (err, availablePlugins)
 */
PluginManager.prototype.getAvailablePlugins = function(callback) {
  var self = this;
  var pluginsDir = config.get()['plugins']['root'];

  async.waterfall([
    function listPluginFiles(callback) {
      fs.readdir(pluginsDir, function(err, files) {
        if (err && err.errno !== constants.ENOENT) {
          callback(err);
          return;
        }

        files = files || [];
        callback(null, files);
      });
    },

    function readPluginsManifest(files, callback) {
      async.forEach(files, function(file, callback) {
        var pluginPath = path.join(pluginsDir, file);
        var pluginName = file;

        if (self._availablePlugins.hasOwnProperty(file)) {
          // Manifest for this file is already cached
          callback();
          return;
        }

        fs.stats(pluginPath, function(err, stats) {
          if (err) {
            log.error('Error while stating plugin file / directory: %s',
                      err.message);
            callback();
            return;
         }

         if (!stats.isDirectory()) {
           // Plugin must be a directory
           callback();
           return;
         }

         self._readPluginManifest(pluginPath, function(err, manifestObj) {
           if (err) {
             log.error('Error while reading plugin manifest: %s', err.message);
             callback();
             return;
           }

           self._availablePlugins[pluginName] = manifestObj;
           callback();
          });
        });

        self._readPluginManifest(pluginPath, function(err, manifestObj) {
          if (err) {
            log.error('Error while reading plugin manifest: %s', err.message);
            callback();
            return;
          }

          self._availablePlugins[pluginName] = manifestObj;
        });
      }, callback);
    }
  ],

  function(err) {
    callback(err, (err) ? null : self._availablePlugins);
  });
};

/**
 * Return an object with the enabled plugins and their settings.
 * @param {Function} callback Callback called with (err, enabledPlugins)
 */
PluginManager.prototype.getEnabledPlugins = function(callback) {
  callback(null, config.get()['plugins']['enabled']);
};

/**
 * Return settings object for the specified plugin.
 *
 * @param {String} pluginName Plugin name.
 * @param {Function} callback Callback called with (err, pluginSettings) where
 *                   pluginSettings is an {Object}
 */
PluginManager.prototype.getPluginSettings = function(pluginName, callback) {
  var self = this;
  var pluginSettings = null;

  async.waterfall([
    async.apply(self.getEnabledPlugins),

    function getPluginSettings(enabledPlugins, callback) {
      if (!enabledPlugins.hasOwnProperty(pluginName)) {
        callback(new Errorf('Plugin "%s" is not enabled'));
        return;
      }

      pluginSettings = enabledPlugins[pluginName];
      callback();
    }
  ],

  function(err) {
    callback(err, pluginSettings);
  });
};

/**
 * Return manifest object for the specified plugin.
 *
 * @param {String} pluginName Plugin name.
 * @param {Function} callback Callback called with (err, pluginManifest) where
 *                   pluginManifest is an {Object}
 */
PluginManager.prototype.getPluginManifest = function(pluginName, callback) {
  var self = this;
  var pluginManifest = null;

  async.waterfall([
    async.apply(self.getAvailablePlugins),

    function getPluginSettings(availablePlugins, callback) {
      if (availablePlugins.hasOwnProperty(pluginName)) {
        callback(new Errorf('Plugin "%s" does not exist'));
        return;
      }

      pluginManifest = availablePlugins[pluginName];
      callback();
    }
  ],

  function(err) {
    callback(err, pluginManifest);
  });
};

/**
 * Read "plugins" part from the settings file and load all the enabled plugins.
 *
 * @param {Function} callback Callback called when the initialization completes.
 */
PluginManager.prototype._initialize = function(callback) {
  var self = this;
  var pluginName, pluginSettings;
  var confPlugins = config.get()['plugins'] || {};
  var enabledPlugins = confPlugins['enabled'];
  var enabledPluginNames = Object.keys(enabledPlugins);

  if (!enabledPlugins) {
    log.info('No plugins have been enabled');
    return;
  }

  async.forEach(enabledPluginNames, function(pluginName, callback) {
    var pluginSettings = enabledPlugins[pluginName];
    self._validatePluginSettings(pluginName, pluginSettings, function onValidated(err) {
      if (err) {
        log.error('Failed to validate plugin "%s" settings: %s', pluginName, err);
        callback();
        return;
      }

      // Load and enable the plugin
      log.info(sprintf('Enabling plugin "%s"', pluginName));
      self.enablePlugin(pluginName, callback);
    });
  }, callback);
};

/**
 * Check if the current version of the agent is supported by the plugin.
 *
 * @param {Object} pluginManifest Plugin manifest (parsed plugin package.json).
 * @return {Bool} true if the version is supported, false otherwise.
 */
PluginManager.prototype.isSupported = function(pluginManifest) {
  var agentVersionConstrain = pluginManifest.hasOwnProperty('agent_version');
  if (!agentVersionConstrain) {
    return true;
  }

  return semver.satisfies(sprintf('%s.%s.%s', version.MAJOR, version.MINOR,
                                  version.PATCH), agentVersionConstrain);
};

/*
 * Validate plugin settings. Validation rules are defined by plugin author in
 * the plugin package.json file.
 *
 * @param {String} pluginName Plugin name.
 * @param {Object} pluginSettings Plugin settings.
 */
PluginManager.prototype.validatePluginSettings = function(pluginName,
                                                          pluginSettings,
                                                          callback) {
  // @TODO: Cache available plugins in this._availablePlugins dictionary
  var self = this;
  var pluginPath = path.join(config.get()['plugins']['root'], pluginName);

  async.waterfall([
    function readPluginManifest(callback) {
      self._readPluginManifest(pluginPath, callback);
    },

    function validatePluginSettings(manifest, callback) {
      var rules = manifest['settings'];
      // @TODO: Use validators from manifest/...
      callback();
    }
  ],

  function(err) {
    callback(err);
  });
};

/**
 * Read plugin manifest file.
 *
 * @param {String} pluginPath Path to the plugin directory.
 * @param {Function} callback Callback called with (err, data)
 */
PluginManager.prototype._readPluginManifest = function(pluginPath, callback) {
  var manifestPath = path.join(pluginPath, pluginConstants.PLUGIN_MANIFEST_NAME);
  fsUtil.jsonFile(manifestPath, callback);
};

/*
 * Enable a plugin and does the following;
 * - Register plugin HTTP endpoints
 * - Register and load plugin services
 * - Register plugin job manager
 * @param {String} pluginName Plugin name.
 * @param {Function} callback Callback called with (err)
 */
PluginManager.prototype.enablePlugin = function(pluginName, callback) {
  var self = this;
  var pluginEndpointPaths = [], pluginServices = [];
  var ops = [];

  var pluginPath = path.join(config.get()['plugins']['root'], pluginName);

  if (misc.inArray(pluginName, Object.keys(this._enabledPlugins))) {
    callback(new Errorf('Plugin "%s" is already enabled'));
    return;
  }

  // Register HTTP endpoints
  function registerHttpEndpoints(endpointRoutes, callback) {
    async.forEach(endpointRoutes, function(endpointRoute, callback) {
      var registeredPaths = self._registerPluginEndpoints(endpointRoute, callback);
      pluginEndpointPaths = pluginEndpointPaths.concat(registeredPaths);
    }, callback);
  }

  // Register and start services
  function registerAndStartServices(services, callback) {
    async.forEach(services, function(service, callback) {
      // pluginServices.push(service.name);
      service.start();
      callback();
    }, callback);
  }

  // Discover all the resources (endpoints, services, jobs) and decide what needs
  // to be done
  async.paralell([
    function discoverHttpEndpoints(callback) {
      pluginsHttpUtils.discoverEndpoints(pluginPath, function onDiscover(err, routes) {
        if (!err && routes) {
          ops.push(async.apply(registerHttpEndpoints, routes));
        }

        callback();
      });
    },

    function discoverServices(callback) {
      pluginServicesUtils.discoverServices(pluginPath, function onDiscover(err, services) {
        if (!err && services) {
          ops.push(async.apply(registerAndStartServices, services));
        }

        callback();
      });
    }

    // @TODO: Discover and register jobs
  ],

  function(err) {
    async.parallel(ops, callback);
  });

  this._enabledPlugins[pluginName] = {
    'endpoints': pluginEndpointPaths, // Store full paths of the plugin endpoints
    'services': pluginServices, // Store names of the plugin services
    'jobs': [] // Store names of the plugin jobs
  };
};

/**
 * Register plugin HTTP endpoint handler with the Cast HTTP server.
 *
 * @param {Array} endpointRoutes Routes array (method, path, middleware, ..., handler)
 * @return {Array} All the paths which have been successfully registered.
 */
PluginManager.prototype._registerPluginEndpoints = function(endpointRoutes) {
  var i, len, route, routeArgsLen, method, path, fullPath, middleware, handler;
  var registerMethod, args;
  var registeredPaths = [];

  for (i = 0, len = endpointRoutes.length; i < len; i++) {
    route = endpointRoutes[i];
    routeArgsLen = route.length;

    if (routeArgsLen < 3) {
      // Invalid route, should we throw?
      continue;
    }

    method = route[0].toLowerCase();
    path = route[1];
    fullPath = sprintf('%s/%s', pluginConstants.HTTP_ENDPOINT_PREFIX, path);

    // Actually register it
    args = [];
    if (routeArgsLen === 3) {
      // No middleware
      middleware = [];
    }
    else {
      middleware = route.splice(2, routeArgsLen - 1);
    }

    handler = route[3];
    registerMethod = httpServer[method];

    if (middleware) {
      args.push(path);
      args = args.concat(middleware);
      args.push(handler);
    }
    else {
      args = [ path, handler ];
    }

    // @TODO: Handle possible error
    registerMethod.apply(httpServer, args);
    registeredPaths.push(fullPath);
  }

  return registeredPaths;
};

/**
 * Disable a plugin.
 *
 * @param {String} pluginName Plugin name.
 * @param {Function} callback Callback called with ()
 */
PluginManager.prototype.disablePlugin = function(pluginName, callback) {
  var i, len;
  if (!misc.inArray(pluginName, Object.keys(this._enabledPlugins))) {
    callback(new Errorf('Plugin "%s" is not enabled'));
    return;
  }

  var plugin = this._enabledPlugins[pluginName];

  async.series([
    // Remove all the plugin HTTP endpoints
    function removeHttpEndpoints(callback) {
      var endpoint;
      var endpoints = plugin['endpoints'];

      for (i = 0, len = endpoints.length; i < len; i++) {
        endpoint = endpoints[i];
        httpServer.remove(endpoint);
      }

      callback();
    },

    // Stop all the services
    function stopAndUnregisterServices(callback) {
      var service;
      var services = plugin['services'];
      for (i = 0, len = services.length; i < len; i++) {
        service = services[i];
        service.stop();
      }
    },

    // Remove jobs. Note all the pending jobs will be canceled.
    function stopAndRemoveJobs(callback) {
      // @TODO:
      callback();
    }
  ], callback);
};

exports.PluginManager = PluginManager;
<|endoftext|>