Source: util/boost-info-parser.js

/**
 * Copyright (C) 2014-2016 Regents of the University of California.
 * @author: Jeff Thompson <jefft0@remap.ucla.edu>
 * From PyNDN boost_info_parser by Adeola Bannis.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * A copy of the GNU Lesser General Public License is in the file COPYING.
 */

/** @ignore */
var fs = require('fs');

/**
 * BoostInfoTree is provided for compatibility with the Boost INFO property list
 * format used in ndn-cxx.
 *
 * Each node in the tree may have a name and a value as well as associated
 * sub-trees. The sub-tree names are not unique, and so sub-trees are stored as
 * dictionaries where the key is a sub-tree name and the values are the
 * sub-trees sharing the same name.
 *
 * Nodes can be accessed with a path syntax, as long as nodes in the path do not
 * contain the path separator '/' in their names.
 * @constructor
 */
var BoostInfoTree = function BoostInfoTree(value, parent)
{
  // subtrees is an array of {key: treeName, value: subtreeList} where
  // treeName is a string and subtreeList is an array of BoostInfoTree.
  // We can't use a dictionary because we want the keys to be in order.
  this.subtrees = [];
  this.value = value;
  this.parent = parent;

  this.lastChild = null;
};

/**
 * Insert a BoostInfoTree as a sub-tree with the given name.
 * @param {string} treeName The name of the new sub-tree.
 * @param {BoostInfoTree} newTree The sub-tree to add.
 */
BoostInfoTree.prototype.addSubtree = function(treeName, newTree)
{
  var subtreeList = this.find(treeName);
  if (subtreeList !== null)
    subtreeList.push(newTree);
  else
    this.subtrees.push({key: treeName, value: [newTree]});

  newTree.parent = this;
  this.lastChild = newTree;
};

/**
 * Create a new BoostInfo and insert it as a sub-tree with the given name.
 * @param {string} treeName The name of the new sub-tree.
 * @param {string} value The value associated with the new sub-tree.
 * @returns {BoostInfoTree} The created sub-tree.
 */
BoostInfoTree.prototype.createSubtree = function(treeName, value)
{
  var newTree = new BoostInfoTree(value, this);
  this.addSubtree(treeName, newTree);
  return newTree;
};

/**
 * Look up using the key and return a list of the subtrees.
 * @param {string} key The key which may be a path separated with '/'.
 * @returns {Array<BoostInfoTree>} A new array with pointers to the subtrees.
 */
BoostInfoTree.prototype.get = function(key)
{
  // Strip beginning '/'.
  key = key.replace(/^\/+/, "");
  if (key.length === 0)
    return [this];
  var path = key.split('/');

  var subtrees = this.find(path[0]);
  if (subtrees === null)
    return [];
  if (path.length == 1)
    return subtrees.slice(0);

  var newPath = path.slice(1).join('/');
  var foundVals = [];
  for (var i = 0; i < subtrees.length; ++i) {
    var t = subtrees[i];
    var partial = t.get(newPath);
    foundVals = foundVals.concat(partial);
  }
  return foundVals;
};

/**
 * Look up using the key and return string value of the first subtree.
 * @param {string} key The key which may be a path separated with '/'.
 * @returns {string} The string value or null if not found.
 */
BoostInfoTree.prototype.getFirstValue = function(key)
{
  var list = this.get(key);
  if (list.length >= 1)
    return list[0].value;
  else
    return null;
};

BoostInfoTree.prototype.getValue = function() { return this.value; };

BoostInfoTree.prototype.getParent = function() { return this.parent; };

BoostInfoTree.prototype.getLastChild = function() { return this.lastChild; };

BoostInfoTree.prototype.prettyPrint = function(indentLevel)
{
  indentLevel = indentLevel || 1;

  var prefix = Array(indentLevel + 1).join(' ');
  var s = "";

  if (this.parent != null) {
    if (this.value && this.value.length > 0)
      s += "\"" + this.value + "\"";
    s += "\n";
  }

  if (this.subtrees.length > 0) {
    if (this.parent)
      s += prefix + "{\n";
    var nextLevel = Array(indentLevel + 2 + 1).join(' ');
    for (var i = 0; i < this.subtrees.length; ++i) {
      for (var iSubTree = 0; iSubTree < this.subtrees[i].value.length; ++iSubTree)
        s += nextLevel + this.subtrees[i].key + " " +
             this.subtrees[i].value[iSubTree].prettyPrint(indentLevel + 2);
    }

    if (this.parent)
      s +=  prefix + "}\n";
  }

  return s;
};

BoostInfoTree.prototype.toString = function()
{
  return this.prettyPrint();
};

/**
 * Use treeName to find the array of BoostInfoTree in this.subtrees.
 * @param {string} treeName The key in this.subtrees to search for. This does a
 * flat search in subtrees_.  It does not split by '/' into a path.
 * @returns {Array<BoostInfoTree>} A array of BoostInfoTree, or null if not found.
 */
BoostInfoTree.prototype.find = function(treeName)
{
  for (var i = 0; i < this.subtrees.length; ++i) {
    if (this.subtrees[i].key == treeName)
      return this.subtrees[i].value;
  }

  return null;
};

/**
 * A BoostInfoParser reads files in Boost's INFO format and constructs a
 * BoostInfoTree.
 * @constructor
 */
var BoostInfoParser = function BoostInfoParser()
{
  this.root = new BoostInfoTree();
};

exports.BoostInfoParser = BoostInfoParser;
exports.BoostInfoTree = BoostInfoTree; // debug

/**
 * Add the contents of the file or input string to the root BoostInfoTree. There
 * are two forms:
 * read(fileName) reads fileName from the file system.
 * read(input, inputName) reads from the input, in which case inputName is used
 * only for log messages, etc.
 * @param {string} fileName The path to the INFO file.
 * @param {string} input The contents of the INFO file, with lines separated by
 * "\n" or "\r\n".
 * @param {string} inputName Use with input for log messages, etc.
 */
BoostInfoParser.prototype.read = function(fileNameOrInput, inputName)
{
  var input;
  if (typeof inputName == 'string')
    input = fileNameOrInput;
  else {
    // No inputName, so assume the first arg is the file name.
    var fileName = fileNameOrInput;
    inputName = fileName;
    input = fs.readFileSync(fileName).toString();
  }

  var ctx = this.root;
  var thisParser = this;
  input.split(/\r?\n/).forEach(function(line) {
    ctx = thisParser.parseLine(line.trim(), ctx);
  });
};

/**
 * Write the root tree of this BoostInfoParser as file in Boost's INFO format.
 * @param {string} fileName The output path.
 */
BoostInfoParser.prototype.write = function(fileName)
{
  fs.writeFileSync(fileName, "" + this.root);
};

/**
 * Get the root tree of this parser.
 * @return {BoostInfoTree} The root BoostInfoTree.
 */
BoostInfoParser.prototype.getRoot = function() { return this.root; };

/**
 * Similar to Python's shlex.split, split s into an array of strings which are
 * separated by whitespace, treating a string within quotes as a single entity
 * regardless of whitespace between the quotes. Also allow a backslash to escape
 * the next character.
 * @param {string} s The input string to split.
 * @returns {Array<string>} An array of strings.
 */
BoostInfoParser.shlex_split = function(s)
{
  var result = [];
  if (s == "")
    return result;
  var whiteSpace = " \t\n\r";
  var iStart = 0;

  while (true) {
    // Move iStart past whitespace.
    while (whiteSpace.indexOf(s[iStart]) >= 0) {
      iStart += 1;
      if (iStart >= s.length)
        // Done.
        return result;
    }

    // Move iEnd to the end of the token.
    var iEnd = iStart;
    var inQuotation = false;
    var token = "";
    while (true) {
      if (s[iEnd] == '\\') {
        // Append characters up to the backslash, skip the backslash and
        //   move iEnd past the escaped character.
        token += s.substring(iStart, iEnd);
        iStart = iEnd + 1;
        iEnd = iStart;
        if (iEnd >= s.length)
          // An unusual case: A backslash at the end of the string.
          break;
      }
      else {
        if (inQuotation) {
          if (s[iEnd] == '\"') {
            // Append characters up to the end quote and skip.
            token += s.substring(iStart, iEnd);
            iStart = iEnd + 1;
            inQuotation = false;
          }
        }
        else {
          if (s[iEnd] == '\"') {
            // Append characters up to the start quote and skip.
            token += s.substring(iStart, iEnd);
            iStart = iEnd + 1;
            inQuotation = true;
          }
          else
            if (whiteSpace.indexOf(s[iEnd]) >= 0)
              break;
        }
      }

      iEnd += 1;
      if (iEnd >= s.length)
        break;
    }

    token += s.substring(iStart, iEnd);
    result.push(token);
    if (iEnd >= s.length)
      // Done.
      return result;

    iStart = iEnd;
  }
};

BoostInfoParser.prototype.parseLine = function(line, context)
{
  // Skip blank lines and comments.
  var commentStart = line.indexOf(';');
  if (commentStart >= 0)
    line = line.substring(0, commentStart).trim();
  if (line.length == 0)
    return context;

  // Usually we are expecting key and optional value.
  var strings = BoostInfoParser.shlex_split(line);
  var isSectionStart = false;
  var isSectionEnd = false;
  for (var i = 0; i < strings.length; ++i) {
    isSectionStart = (isSectionStart || strings[i] == "{");
    isSectionEnd = (isSectionEnd || strings[i] == "}");
  }

  if (!isSectionStart && !isSectionEnd) {
    var key = strings[0];
    var val;
    if (strings.length > 1)
      val = strings[1];
    context.createSubtree(key, val);

    return context;
  }

  // OK, who is the joker who put a { on the same line as the key name?!
  var sectionStart = line.indexOf('{');
  if (sectionStart > 0) {
    var firstPart = line.substring(0, sectionStart);
    var secondPart = line.substring(sectionStart);

    var ctx = this.parseLine(firstPart, context);
    return this.parseLine(secondPart, ctx);
  }

  // If we encounter a {, we are beginning a new context.
  // TODO: Error if there was already a subcontext here.
  if (line[0] == '{') {
    context = context.getLastChild();
    return context;
  }

  // If we encounter a }, we are ending a list context.
  if (line[0] == '}') {
    context = context.getParent();
    return context;
  }

  throw runtime_error("BoostInfoParser: input line is malformed");
};