<|start_filename|>lib/parser/csv.js<|end_filename|>
/* jshint node:true */
'use strict';

/**
 * Module dependencies.
 */
const joinRows = require('../core/join-rows');
const Handler = require('./handler');
const helper = require('../core/helper');

class Parser {
  constructor(options) {
    this._options = options || {};
    this._handler = new Handler(this._options);
    this._headers = this._options.headers || [];
    this._escape = require('../core/escape-delimiters')(
      this._options.textDelimiter,
      this._options.rowDelimiter,
      this._options.forceTextDelimiter
    );
  }

  /**
   * Generates a CSV file with optional headers based on the passed JSON,
   * with can be an Object or Array.
   *
   * @param {Object|Array} json
   * @param {Function} done(err,csv) - Callback function
   *      if error, returning error in call back.
   *      if csv is created successfully, returning csv output to callback.
   */
  parse(json, done, stream) {
    if (helper.isArray(json)) return done(null, this._parseArray(json, stream));
    else if (helper.isObject(json)) return done(null, this._parseObject(json));
    return done(new Error('Unable to parse the JSON object, its not an Array or Object.'));
  }

  get headers() {
    let headers = this._headers;

    if (this._options.rename && this._options.rename.length > 0)
      headers = headers.map((header) => this._options.rename[this._options.headers.indexOf(header)] || header);
      
    if (this._options.forceTextDelimiter) {
      headers = headers.map((header) => {
        return `${this._options.textDelimiter}${header}${this._options.textDelimiter}`;
      });
    }

    if (this._options.mapHeaders)
      headers = headers.map(this._options.mapHeaders);

    return headers.join(this._options.rowDelimiter);
  }

  _checkRows(rows) {
    let lastRow = null;
    let finalRows = [];
    let fillGaps = (col, index) => col === '' || col === undefined ? lastRow[index] : col;
    for (let row of rows) {
      let missing = this._headers.length - row.length;
      if (missing > 0) row = row.concat(Array(missing).join(".").split("."));
      if (lastRow && this._options.fillGaps) row = row.map(fillGaps);
      finalRows.push(row.join(this._options.rowDelimiter));
      lastRow = row;
    }
    return finalRows;
  }

  _parseArray(json, stream) {
    let self = this;
    this._headers = this._headers || [];
    let fileRows = [];
    let outputFile;
    let fillRows;

    let getHeaderIndex = function(header) {
      var index = self._headers.indexOf(header);
      if (index === -1) {
        self._headers.push(header);
        index = self._headers.indexOf(header);
      }
      return index;
    };

    //Generate the csv output
    fillRows = function(result) {
      const rows = [];
      const fillAndPush = (row) => rows.push(row.map(col => col != null ? col : ''));
      // initialize the array with empty strings to handle 'unpopular' headers
      const newRow = () => new Array(self._headers.length).fill(null);
      const emptyRowIndexByHeader = {};
      let currentRow = newRow();
      for (let element of result) {
        let elementHeaderIndex = getHeaderIndex(element.item);
        if (currentRow[elementHeaderIndex] != undefined) {
          fillAndPush(currentRow);
          currentRow = newRow();
        }
        emptyRowIndexByHeader[elementHeaderIndex] = emptyRowIndexByHeader[elementHeaderIndex] || 0;
        // make sure there isn't a empty row for this header
        if (self._options.fillTopRow && emptyRowIndexByHeader[elementHeaderIndex] < rows.length) {
          rows[emptyRowIndexByHeader[elementHeaderIndex]][elementHeaderIndex] = self._escape(element.value);
          emptyRowIndexByHeader[elementHeaderIndex] += 1;
          continue;
        }
        currentRow[elementHeaderIndex] = self._escape(element.value);
        emptyRowIndexByHeader[elementHeaderIndex] += 1;
      }
      // push last row
      if (currentRow.length > 0) {
        fillAndPush(currentRow);
      }
      fileRows = fileRows.concat(self._checkRows(rows));
    };
    for (let item of json) {
      //Call checkType to list all items inside this object
      //Items are returned as a object {item: 'Prop Value, Item Name', value: 'Prop Data Value'}
      let itemResult = self._handler.check(item, self._options.mainPathItem, item, json);
      fillRows(itemResult);
    }

    if (!stream && self._options.includeHeaders) {
      //Add the headers to the first line
      fileRows.unshift(this.headers);
    }

    return joinRows(fileRows, self._options.endOfLine);
  }

  _parseObject(json) {
    let self = this;
    let fileRows = [];
    let parseResult = [];
    let outputFile;
    let fillRows;
    let horizontalRows = [
      [],
      []
    ];

    fillRows = function(result) {
      var value = result.value || result.value === 0 ? result.value.toString() : self._options.undefinedString;
      value = self._escape(value);

      //Type header;value
      if (self._options.verticalOutput) {
        var row = [result.item, value];
        fileRows.push(row.join(self._options.rowDelimiter));
      } else {
        horizontalRows[0].push(result.item);
        horizontalRows[1].push(value);
      }
    };
    for (var prop in json) {
      var prefix = "";
      if (this._options.mainPathItem)
        prefix = this._options.mainPathItem + this._options.headerPathString;
      parseResult = this._handler.check(json[prop], prefix + prop, prop, json);

      parseResult.forEach(fillRows);
    }
    if (!this._options.verticalOutput) {
      fileRows.push(horizontalRows[0].join(this._options.rowDelimiter));
      fileRows.push(horizontalRows[1].join(this._options.rowDelimiter));
    }
    return joinRows(fileRows, this._options.endOfLine);
  }
}

module.exports = Parser;


<|start_filename|>dist/parser/handler.js<|end_filename|>
/* jshint node:true */
'use strict';

var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var helper = require('../core/helper');

var Handler = function () {
  function Handler(options) {
    _classCallCheck(this, Handler);

    this._options = options;

    // an object of {typeName:(value,index,parent)=>any}
    this._options.typeHandlers = this._options.typeHandlers || {};
  }

  /**
   * Check if results needing mapping to alternate value
   *
   * @returns [{item, value}] result
   */


  _createClass(Handler, [{
    key: '_setHeaders',
    value: function _setHeaders(result, item) {
      var self = this;
      if (!item) return result;
      return result.map(function (element) {
        element.item = element.item ? item + self._options.headerPathString + element.item : item;
        return element;
      });
    }
  }, {
    key: 'castValue',
    value: function castValue(element, item, index, parent) {
      //cast by matching constructor
      var types = this._options.typeHandlers;
      for (var type in types) {
        if (isInstanceOfTypeName(element, type)) {
          element = types[type].call(types, element, index, parent);
          break; //first match we move on
        }
      }

      return element;
    }
  }, {
    key: 'checkComplex',
    value: function checkComplex(element, item) {
      //Check if element is a Date
      if (helper.isDate(element)) {
        return [{
          item: item,
          value: (this._options.handleDate || this._handleDate)(element, item)
        }];
      }
      //Check if element is an Array
      else if (helper.isArray(element)) {
          var resultArray = this._handleArray(element, item);
          return this._setHeaders(resultArray, item);
        }
        //Check if element is a Object
        else if (helper.isObject(element)) {
            var resultObject = this._handleObject(element);
            return this._setHeaders(resultObject, item);
          }

      return [{
        item: item,
        value: ''
      }];
    }

    /**
     * Check the element type of the element call the correct handle function
     *
     * @param element Element that will be checked
     * @param item Used to make the headers/path breadcrumb
     * @returns [{item, value}] result
     */

  }, {
    key: 'check',
    value: function check(element, item, index, parent) {
      element = this.castValue(element, item, index, parent);
      // try simple value by highier performance switch
      switch (typeof element === 'undefined' ? 'undefined' : _typeof(element)) {
        case 'string':
          return [{
            item: item,
            value: this._handleString(element, item)
          }];

        case 'number':
          return [{
            item: item,
            value: this._handleNumber(element, item)
          }];

        case 'boolean':
          return [{
            item: item,
            value: this._handleBoolean.bind(this)(element, item)
          }];
      }

      return this.checkComplex(element, item);
    }

    /**
     * Handle all Objects
     *
     * @param {Object} obj
     * @returns [{item, value}] result
     */

  }, {
    key: '_handleObject',
    value: function _handleObject(obj) {
      var result = [];
      //Look every object props
      for (var prop in obj) {
        var propData = obj[prop];
        //Check the propData type
        var resultCheckType = this.check(propData, prop, prop, obj);
        //Append to results aka merge results aka array-append-array
        result = result.concat(resultCheckType);
      }
      return result;
    }

    /**
     * Handle all Arrays, merges arrays with primitive types in a single value
     *
     * @param {Array} array
     * @returns [{item, value}] result
     */

  }, {
    key: '_handleArray',
    value: function _handleArray(array) {
      var self = this;
      var result = [];
      var firstElementWithoutItem;
      for (var aIndex = 0; aIndex < array.length; ++aIndex) {
        var element = array[aIndex];
        //Check the propData type
        var resultCheckType = self.check(element, null, aIndex, array);
        //Check for results without itens, merge all itens with the first occurrence
        if (resultCheckType.length === 0) continue;
        var firstResult = resultCheckType[0];
        if (!firstResult.item && firstElementWithoutItem !== undefined) {
          firstElementWithoutItem.value += self._options.arrayPathString + firstResult.value;
          continue;
        } else if (resultCheckType.length > 0 && !firstResult.item && firstElementWithoutItem === undefined) {
          firstElementWithoutItem = firstResult;
        }
        //Append to results
        result = result.concat(resultCheckType);
      }
      return result;
    }
    /**
     * Handle all Boolean variables, can be replaced with options.handleBoolean
     *
     * @param {Boolean} boolean
     * @returns {String} result
     */

  }, {
    key: '_handleBoolean',
    value: function _handleBoolean(boolean) {
      var result;
      //Check for booolean options
      if (boolean) {
        result = this._options.booleanTrueString || 'true';
      } else {
        result = this._options.booleanFalseString || 'false';
      }
      return result;
    }
    /**
     * Handle all String variables, can be replaced with options.handleString
     *
     * @param {String} string
     * @returns {String} string
     */

  }, {
    key: '_handleString',
    value: function _handleString(string) {
      return string;
    }
    /**
     * Handle all Number variables, can be replaced with options.handleNumber
     *
     * @param {Number} number
     * @returns {Number} number
     */

  }, {
    key: '_handleNumber',
    value: function _handleNumber(number) {
      return number;
    }
    /**
     * Handle all Date variables, can be replaced with options.handleDate
     *
     * @param {Date} number
     * @returns {string} result
     */

  }, {
    key: '_handleDate',
    value: function _handleDate(date) {
      return date.toLocaleDateString();
    }
  }]);

  return Handler;
}();

module.exports = Handler;

var globalScope = typeof window === "undefined" ? global : window;
function isInstanceOfTypeName(element, typeName) {
  if (element instanceof globalScope[typeName]) {
    return true; //Buffer and complex objects
  }

  //literals in javascript cannot be checked by instance of
  switch (typeof element === 'undefined' ? 'undefined' : _typeof(element)) {
    case 'string':
      return typeName === "String";
    case 'boolean':
      return typeName === "Boolean";
    case 'number':
      return typeName === "Number";
  }

  return false;
}

<|start_filename|>tests/array.js<|end_filename|>
/* jshint node:true */
/* jshint esversion: 6 */
/* jshint -W030 */

const {assert} = require('chai');
const jsonexport = require('../lib/index');
const os = require('os');

describe('Array', () => {
  it('simple', async () => {
    const csv = await jsonexport([{
      name: 'Bob',
      lastname: 'Smith'
    }, {
      name: 'James',
      lastname: 'David',
      escaped: 'I am a "quoted" field'
    }], {})
    assert.equal(csv, `name,lastname,escaped${os.EOL}Bob,Smith${os.EOL}James,David,"I am a ""quoted"" field"`);
  });
  it('complex', async () => {
    const csv = await jsonexport([{
      id: 1,
      name: 'Bob',
      lastname: 'Smith',
      family: {
        name: 'Peter',
        type: 'Father'
      }
    }, {
      id: 2,
      name: 'James',
      lastname: 'David',
      family: {
        name: 'Julie',
        type: 'Mother'
      }
    }], {})

    assert.equal(csv, `id,name,lastname,family.name,family.type${os.EOL}1,Bob,Smith,Peter,Father${os.EOL}2,James,David,Julie,Mother`);
  });
  it('with nested arrays', async () => {
    const csv = await jsonexport([{
      a: {
        b: true,
        c: [{
            d: 1
          },
          {
            d: 2
          },
          {
            d: 3
          },
          {
            d: 4
          }
        ],
        e: [{
            f: 1
          },
          {
            f: 2
          }
        ]
      }
    }], {})

    assert.equal(csv, `a.b,a.c.d,a.e.f${os.EOL}true,1,${os.EOL},2,${os.EOL},3,${os.EOL},4,1${os.EOL},,2`)
  });
  it('with nested arrays & empty strings', async () => {
    const csv = await jsonexport([
      {
        "a": "",
        "b": "b",
        "c": [
          {
            "a": "a1",
            "b": "b1"
          },
          {
            "a": "a2",
            "b": "b2"
          },
          {
            "a": "",
            "b": "b3"
          },
          {
            "a": "a4",
            "b": "b4"
          }
        ]
      }
    ], {})

    assert.equal(csv, `a,b,c.a,c.b${os.EOL},b,a1,b1${os.EOL},,a2,b2${os.EOL},,,b3${os.EOL},,a4,b4`)
  });
});
