Source: lib/kss_styleguide.js

'use strict';

/**
 * The `kss/lib/kss_styleguide` module is normally accessed via the
 * [`KssStyleguide()`]{@link module:kss.KssStyleguide} constructor of the `kss`
 * module:
 * ```
 * var kss = require('kss');
 * var styleGuide = new kss.KssStyleguide(data);
 * ```
 * @private
 * @module kss/lib/kss_styleguide
 */

var KssStyleguide;

/**
 * An instance of this class is returned on finishing `kss.traverse`. It has a
 * few convenience methods for interpreting data, the most important of which is
 * section() for searching for specific sections of the style guide.
 *
 * @constructor
 * @alias module:kss.KssStyleguide
 * @param {Object} data The data object generated by traverse.
 */
KssStyleguide = function(data) {
  if (!(this instanceof KssStyleguide)) {
    return new KssStyleguide();
  }
  this.data = data || {};
  this.referenceDelimiter = this.data.referenceDelimiter;
  this.data.weightMap = false;
  this.init();
};

/**
 * Initializes the object and data passed to the constructor. Called
 * automatically from the KssStyleguide() constructor; should not be called
 * directly.
 * @private
 */
KssStyleguide.prototype.init = function() {
  var i, l, incrementIndex, autoIncrement = [0], ref, previousRef = [], index;

  if (this.data.sections) {
    // Sort all the sections.
    this.sortSections();

    // Loop through all the sections to initialize some computed values.
    l = this.data.sections.length;
    for (i = 0; i < l; i += 1) {
      // Set the styleguide property on each KssSection object.
      this.data.sections[i].styleguide = this;

      ref = this.data.sections[i].reference();
      // Set the referenceURI value.
      this.data.sections[i].data.referenceURI = this.data.sections[i].encodeReferenceURI(ref);

      // Compare the previous Ref to the new Ref.
      ref = ref.split(this.referenceDelimiter);
      // If they are already equal, we don't need to increment the section number.
      if (previousRef.join(this.referenceDelimiter) !== ref.join(this.referenceDelimiter)) {
        incrementIndex = 0;
        for (index = 0; index < previousRef.length; index += 1) {
          // Find the index where the refs differ.
          if (index >= ref.length || previousRef[index] !== ref[index]) {
            break;
          }
          incrementIndex = index + 1;
        }
        if (incrementIndex < autoIncrement.length) {
          // Increment the part where the refs started to differ.
          autoIncrement[incrementIndex]++;
          // Trim off the extra parts of the autoincrement where the refs differed.
          autoIncrement = autoIncrement.slice(0, incrementIndex + 1);
        }
        // Add parts to the autoincrement to ensure it is the same length as the new ref.
        for (index = autoIncrement.length; index < ref.length; index += 1) {
          autoIncrement[index] = 1;
        }
      }
      this.data.sections[i].data.autoincrement = autoIncrement.join('.');
      previousRef = ref;
    }
  }
};

/**
 * Search for sections within the style guide.
 *
 * There's a few ways to use this method:
 * - `section()` returns all of the sections.
 *
 * Using strings:
 * - `section('2')` returns Section 2.
 * - `section('2.*')` returns Section 2 and all of its descendants.
 * - `section('2.x')` returns Section 2's children only.
 * - `section('2.x.x')` returns Section 2's children, and their children too.
 *
 * Or Regular Expressions:
 * - `section(/2\.[1-5]/)` returns Sections 2.1 through to 2.5.
 *
 * @param {string|Regexp} query A string or Regexp object to match a KssSection's style guide reference.
 * @returns {false|KssSection|Array} The exact KssSection requested, an array of KssSection objects matching the query, or false.
 */
KssStyleguide.prototype.section = function(query) {
  var i, l = this.data.sections.length,
    current, isRegexSearch, delim,
    sections = [],
    match;

  query = query || /.+/;

  // Exact queries.
  if (typeof query === 'string') {
    // If the query is '*', 'x', or ends with '.*', ' - *', '.x', or ' - x',
    // then it is not an exact query.
    isRegexSearch = query.match(/(^[x\*]$|\s\-\s[x\*]$|\.[x\*]$)/);
    if (!isRegexSearch) {
      if (this.data.section_refs && this.data.section_refs[query]) {
        return this.data.section_refs[query];
      } else {
        return false;
      }
    }
  }

  // Convert regex strings into proper JavaScript RegExp objects.
  if (!(query instanceof RegExp)) {
    delim = this.referenceDelimiter === '.' ? '\\.' : '\\ \\-\\ ';
    query = new RegExp(
      query
        // Convert '*' to a simple .+ regex.
        .replace(/^\*$/, '.+')
        // Convert 'x' to a regex matching one level of reference.
        .replace(/^x$/, '^.+?(?=($|' + delim + '))')
        // Convert '.*' or ' - *' to a ([delim].+){0,1} regex.
        .replace(/(\.|\s+\-\s+)\*$/g, '(' + delim + '.+){0,1}')
        // Convert the first '.x' or ' - x' to a regex matching one sub-level
        // of a reference.
        .replace(/(\.|\s+\-\s+)x\b/, delim + '.+?(?=($|' + delim + '))')
        // Convert any remaining '.x' or ' - x' to a regex matching zero or one
        // sub-levels of a reference.
        .replace(/(\.|\s+\-\s+)x\b/g, '(' + delim + '.+?(?=($|' + delim + '))){0,1}')
        // Convert any remaining '-' into '\-'
        .replace(/([^\\])\-/g, '$1\\-')
    );
  }

  // General (regex) search
  for (i = 0; i < l; i += 1) {
    current = this.data.sections[i];
    match = current.data.reference.match(query);
    if (match && match[0] === current.data.reference) {
      sections.push(current);
    }
  }

  if (!sections) {
    return false;
  }

  return sections;
};

/**
 * Sorts the sections of the style guide.
 *
 * This is called automatically when the {@link KssStyleguide} object is
 * created, but is publicly accessible as well.
 */
KssStyleguide.prototype.sortSections = function() {
  var self = this;

  // Sort sections based on reference number.
  this.data.sections.sort(function(a, b) {
    // Split the 2 references into chunks by their period or dash seperators.
    var refsA = a.data.reference.toLowerCase().split(self.referenceDelimiter),
      refsB = b.data.reference.toLowerCase().split(self.referenceDelimiter),
      weightA, weightB,
      i, l = Math.max(refsA.length, refsB.length);

    // Compare each set of chunks until we know which reference should be listed first.
    for (i = 0; i < l; i += 1) {
      if (refsA[i] && refsB[i]) {
        // If the 2 chunks are unequal, compare them.
        if (refsA[i] !== refsB[i]) {
          // If the chunks have different weights, sort by weight.
          weightA = self.getWeight(a.reference(), i);
          weightB = self.getWeight(b.reference(), i);
          if (weightA !== weightB) {
            return weightA - weightB;
          } else if (refsA[i].match(/^\d+$/) && refsB[i].match(/^\d+$/)) {
            // If both chunks are digits, use numeric sorting.
            return refsA[i] - refsB[i];
          } else {
            // Otherwise, use alphabetical string sorting.
            return (refsA[i] > refsB[i]) ? 1 : -1;
          }
        }
      } else {
        // If 1 of the chunks is empty, it goes first.
        return refsA[i] ? 1 : -1;
      }
    }

    return 0;
  });
};

/**
 * Helper function for `KssStyleguide.sortSections()` that gets the weight of the
 * given reference at the given depth.
 *
 * @param {string} reference A section reference.
 * @param {int} depth The requested depth.
 * @returns {int} The requested weight.
 */
KssStyleguide.prototype.getWeight = function(reference, depth) {
  // Initialize the map of weights.
  if (this.data.weightMap === false) {
    this.data.weightMap = {};
    /*eslint-disable guard-for-in*/
    for (var key in this.data.sections) {
      this.data.weightMap[this.data.sections[key].reference().toLowerCase()] = this.data.sections[key].weight();
    }
    /*eslint-enable guard-for-in*/
  }

  reference = reference.toLowerCase();
  if (typeof depth !== 'undefined') {
    reference = reference.split(this.referenceDelimiter, depth + 1).join(this.referenceDelimiter);
  }

  return this.data.weightMap[reference] ? this.data.weightMap[reference] : 0;
};

module.exports = KssStyleguide;