Source: lib/kss_section.js

'use strict';

const KssModifier = require('./kss_modifier'),
  KssParameter = require('./kss_parameter');

/**
 * The `kss/lib/kss_section` module is normally accessed via the
 * [`KssSection()`]{@link module:kss.KssSection} class of the `kss` module:
 * ```
 * const KssSection = require('kss').KssSection;
 * ```
 * @private
 * @module kss/lib/kss_section
 */

/**
 * A KssSection object represents a single section of a `KssStyleGuide`.
 *
 * A section of a style guide can be used for:
 * - a category of sub-sections, since sections are hierarchical
 * - a component in a design
 * - a mixin (or similar concept) of a CSS preprocessor
 *
 * This class is normally accessed via the [`kss`]{@link module:kss} module:
 * ```
 * const KssSection = require('kss').KssSection;
 * ```
 *
 * @alias module:kss.KssSection
 */
class KssSection {

  /**
   * Creates a KssSection object and stores the given data.
   *
   * If passed an object, it will add the properties to the section.
   *
   * @param {Object} [data] An object of data.
   */
  constructor(data) {
    data = data || {};

    this.meta = {
      styleGuide: data.styleGuide || null,
      raw: data.raw || '',
      customPropertyNames: [],
      depth: data.depth || 0
    };

    this.data = {
      header: '',
      description: '',
      deprecated: false,
      experimental: false,
      reference: '',
      referenceNumber: '',
      referenceURI: '',
      weight: 0,
      markup: '',
      colors: [],
      source: {
        filename: '',
        path: '',
        line: ''
      },
      modifiers: [],
      parameters: []
    };

    // Loop through the given properties.
    for (let name in data) {
      // istanbul ignore else
      if (data.hasOwnProperty(name)) {
        // If the property is defined in this.data, add it via our API.
        if (this.data.hasOwnProperty(name)) {
          this[name](data[name]);

          // If the property isn't defined in meta or data, add a custom property.
        } else if (!this.meta.hasOwnProperty(name)) {
          this.custom(name, data[name]);
        }
      }
    }
  }

  /**
   * Return the `KssSection` as a JSON object.
   *
   * @returns {Object} A JSON object representation of the KssSection.
   */
  toJSON() {
    /* eslint-disable key-spacing */
    let returnObject = {
      header: this.header(),
      description: this.description(),
      deprecated: this.deprecated(),
      experimental: this.experimental(),
      reference: this.reference(),
      referenceNumber: this.referenceNumber(),
      referenceURI: this.referenceURI(),
      weight: this.weight(),
      colors: this.colors(),
      markup: this.markup(),
      source: this.source(),
      // Include meta as well.
      depth: this.depth()
    };
    /* eslint-enable key-spacing */

    returnObject.modifiers = this.modifiers().map(modifier => {
      return modifier.toJSON();
    });
    returnObject.parameters = this.parameters().map(parameter => {
      return parameter.toJSON();
    });

    // Add custom properties to the JSON object.
    for (let i = 0; i < this.meta.customPropertyNames.length; i++) {
      // istanbul ignore else
      if (typeof this.custom(this.meta.customPropertyNames[i]) !== 'undefined') {
        returnObject[this.meta.customPropertyNames[i]] = this.custom(this.meta.customPropertyNames[i]);
      }
    }

    return returnObject;
  }

  /**
   * Gets or sets the `KssStyleGuide` object this `KssSection` is associated with.
   *
   * If the `styleGuide` value is provided, the `KssStyleGuide` for this section
   * is set. Otherwise, the `KssStyleGuide` of the section is returned.
   *
   * @param {KssStyleGuide} [styleGuide] Optional. The `KssStyleGuide` that owns the
   *   `KssSection`.
   * @returns {KssStyleGuide|KssSection} If styleGuide is given, the current
   *   `KssSection` object is returned to allow chaining of methods. Otherwise,
   *   the `KssStyleGuide` object the section belongs to is returned.
   */
  styleGuide(styleGuide) {
    if (typeof styleGuide === 'undefined') {
      return this.meta.styleGuide;
    }

    this.meta.styleGuide = styleGuide;
    // Tell the style guide about this section's custom property names.
    this.meta.styleGuide.customPropertyNames(this.customPropertyNames());
    // Allow chaining.
    return this;
  }

  /**
   * Gets or sets the header of the section, i.e. the first line in the description.
   *
   * If the `header` value is provided, the `header` for this section is set.
   * Otherwise, the `header` of the section is returned.
   *
   * @param {string} [header] Optional. The header of the section.
   * @returns {KssSection|string} If `header` is given, the `KssSection` object is
   *   returned to allow chaining of methods. Otherwise, the header of the section
   *   is returned.
   */
  header(header) {
    if (typeof header === 'undefined') {
      return this.data.header;
    }

    this.data.header = header;
    // Allow chaining.
    return this;
  }

  /**
   * Gets or sets the description of the section.
   *
   * If the `description` value is provided, the `description` for this section is
   * set. Otherwise, the `description` of the section is returned.
   *
   * @param {string} [description] Optional. The description of the section.
   * @returns {KssSection|string} If `description` is given, the `KssSection`
   *   object is returned to allow chaining of methods. Otherwise, the description
   *   of the section is returned.
   */
  description(description) {
    if (typeof description === 'undefined') {
      return this.data.description;
    }

    this.data.description = description;
    // Allow chaining.
    return this;
  }

  /**
   * Gets the list of custom properties of the section.
   *
   * Note that this method will return the actual custom properties set for this
   * section, and not all of the custom properties available for the entire style
   * guide. Use KssStyleGuide.customPropertyNames() for that list.
   *
   * @returns {string[]} An array of the section's custom property names.
   */
  customPropertyNames() {
    return this.meta.customPropertyNames;
  }

  /**
   * Gets or sets a custom property of the section.
   *
   * If the `value` is provided, the requested custom property of the section is
   * set. Otherwise, the section's custom property with the name specified in the
   * `name` parameter is returned.
   *
   * @param {string} name The name of the section's custom property.
   * @param {*} [value] Optional. The value of the section's custom property.
   * @returns {KssSection|*} If `value` is given, the `KssSection` object is
   *   returned to allow chaining of methods. Otherwise, the section's custom
   *   property, `name`, is returned.
   */
  custom(name, value) {
    if (typeof value === 'undefined') {
      /* eslint-disable no-undefined */
      return this.meta.customPropertyNames.indexOf(name) === -1 ? undefined : this.data[name];
    }

    if (this.styleGuide()) {
      this.styleGuide().customPropertyNames(name);
    }
    this.meta.customPropertyNames.push(name);
    this.data[name] = value;
    // Allow chaining.
    return this;
  }

  /**
   * Gets or sets the deprecated flag for the section.
   *
   * If the `deprecated` value is provided, the `deprecated` flag for this section
   * is set. Otherwise, the `deprecated` flag for the section is returned.
   *
   * @param {boolean} [deprecated] Optional. The deprecated flag for the section.
   * @returns {KssSection|boolean} If `deprecated` is given, the `KssSection`
   *   object is returned to allow chaining of methods. Otherwise, the deprecated
   *   flag for the section is returned.
   */
  deprecated(deprecated) {
    if (typeof deprecated === 'undefined') {
      return this.data.deprecated;
    }

    this.data.deprecated = !!deprecated;
    // Allow chaining.
    return this;
  }

  /**
   * Gets or sets the experimental flag for the section.
   *
   * If the `experimental` value is provided, the `experimental` flag for this
   * section is set. Otherwise, the `deprecated` flag for the section is returned.
   *
   * @param {boolean} [experimental] Optional. The experimental flag for the
   *   section.
   * @returns {KssSection|boolean} If `experimental` is given, the `KssSection`
   *   object is returned to allow chaining of methods. Otherwise, the
   *   experimental flag for the section is returned.
   */
  experimental(experimental) {
    if (typeof experimental === 'undefined') {
      return this.data.experimental;
    }

    this.data.experimental = !!experimental;
    // Allow chaining.
    return this;
  }

  /**
   * Gets or sets the reference for the section.
   *
   * If the `reference` value is provided, the `reference` for this section is
   * set. Otherwise, the `reference` for the section is returned.
   *
   * @param {string} [reference] Optional. The reference of the section.
   * @returns {KssSection|string} If `reference` is given, the `KssSection` object
   *   is returned to allow chaining of methods. Otherwise, the reference for the
   *   section is returned.
   */
  reference(reference) {
    if (typeof reference === 'undefined') {
      return this.data.reference;
    }

    // @TODO: Tell the KssStyleGuide about the update.

    reference = reference.toString();
    // Normalize any " - " delimiters.
    reference = reference.replace(/\s+\-\s+/g, ' - ');
    // Remove trailing dot-zeros and periods.
    reference = reference.replace(/\.$|(\.0){1,}$/g, '');

    this.data.reference = reference;
    // Allow chaining.
    return this;
  }

  /**
   * Gets or sets a numeric reference number for the section.
   *
   * If the `referenceNumber` value is provided, the `referenceNumber` for this
   * section is set.
   *
   * If no parameters are given, this method returns a numeric reference number;
   * if the style guide's references are already numeric (e.g. 2, 2.1.3, 3.2),
   * then this method returns the same value as reference() does. Otherwise, an
   * auto-incremented reference number will be returned.
   *
   * @param {string} [referenceNumber] Optional. The auto-incremented reference
   *   number of the section.
   * @returns {KssSection|string} If `referenceNumber` is given, the `KssSection`
   *   object is returned to allow chaining of methods. Otherwise, the reference
   *   number of the section is returned.
   */
  referenceNumber(referenceNumber) {
    if (typeof referenceNumber === 'undefined') {
      if (this.styleGuide() && this.styleGuide().hasNumericReferences()) {
        return this.data.reference;
      } else {
        return this.data.referenceNumber;
      }
    }

    this.data.referenceNumber = referenceNumber;
    // Allow chaining.
    return this;
  }

  /**
   * Gets or sets the reference of the section, encoded as a valid URI fragment.
   *
   * If the `referenceURI` value is provided, the `referenceURI` for this section
   * is set. Otherwise, the `referenceURI` of the section is returned.
   *
   * @param {string} [referenceURI] Optional. The referenceURI of the section.
   * @returns {KssSection|string} If `referenceURI` is given, the `KssSection`
   *   object is returned to allow chaining of methods. Otherwise, the
   *   referenceURI of the section is returned.
   */
  referenceURI(referenceURI) {
    if (typeof referenceURI === 'undefined') {
      if (!this.data.referenceURI) {
        this.data.referenceURI = encodeURI(
          this.reference()
            .replace(/ \- /g, '-')
            .replace(/[^\w-]+/g, '-')
            .toLowerCase()
        );
      }
      return this.data.referenceURI;
    }

    this.data.referenceURI = referenceURI;
    // Allow chaining.
    return this;
  }

  /**
   * Gets or sets the weight of the section.
   *
   * If the `weight` value is provided, the `weight` for this section is set.
   * Otherwise, the `weight` of the section is returned.
   *
   * @param {number} [weight] Optional. The weight of the section as an integer.
   * @returns {KssSection|number} If `weight` is given, the `KssSection` object
   *   is returned to allow chaining of methods. Otherwise, the weight of the
   *   section is returned.
   */
  weight(weight) {
    if (typeof weight === 'undefined') {
      return this.data.weight;
    }

    // @TODO: The weight needs to bubble-up to the KssStyleGuide weightMap.
    this.data.weight = weight;
    // Allow chaining.
    return this;
  }

  /**
   * Gets or sets the depth of the section.
   *
   * If the `depth` value is provided, the `depth` for this section is set.
   * Otherwise, the `depth` of the section is returned.
   *
   * @param {number} [depth] Optional. The depth of the section as a positive
   *   integer.
   * @returns {KssSection|number} If `depth` is given, the `KssSection` object is
   *   returned to allow chaining of methods. Otherwise, the depth of the section
   *   is returned.
   */
  depth(depth) {
    if (typeof depth === 'undefined') {
      return this.meta.depth;
    }

    this.meta.depth = depth;
    // Allow chaining.
    return this;
  }

  /**
   * Gets or sets the markup of the section.
   *
   * If the `markup` value is provided, the `markup` for this section is set.
   * Otherwise, the `markup` of the section is returned.
   *
   * @param {string} [markup] Optional. The markup of the section.
   * @returns {KssSection|string|boolean} If `markup` is given, the `KssSection` object is
   *   returned to allow chaining of methods. Otherwise, the markup of the section
   *   is returned, or `false` if none.
   */
  markup(markup) {
    if (typeof markup === 'undefined') {
      return this.data.markup;
    }

    this.data.markup = markup;
    // Allow chaining.
    return this;
  }

  /**
   * Gets or sets the colors of the section.
   *
   * If the `colors` value is provided, the `colors` for this section is set.
   * Otherwise, the `colors` of the section is returned.
   *
   * @param {Array} [colors] Collection of color object.
   * @returns {KssSection|string|boolean} If `colors` is given, the `KssSection` object is
   *   returned to allow chaining of methods. Otherwise, the colors of the section
   *   is returned, or `false` if none.
   */
  colors(colors) {
    if (typeof colors === 'undefined') {
      return this.data.colors;
    }

    this.data.colors = colors;
    // Allow chaining.
    return this;
  }

  /**
   * Gets or sets the file information of the file where the section was
   * originally found.
   *
   * If the `source` parameter is provided, the `source` for this section is
   * set. Otherwise, the `source` of the section is returned.
   *
   * The `source` object contains the following information:
   * - filename: The name of the file.
   * - path: The full path of the file.
   * - line: The line number where the KSS comment is found.
   *
   * @param {{filename, path, line}} [source] The source file information where
   *   the section was originally found.
   * @returns {KssSection|{filename, path, line}} If `source` is given, the
   *   `KssSection` object is returned to allow chaining of methods. Otherwise,
   *   the source of the section is returned.
   */
  source(source) {
    if (typeof source === 'undefined') {
      return this.data.source;
    }

    if (source.filename) {
      this.data.source.filename = source.filename;
    }
    if (source.path) {
      this.data.source.path = source.path;
    }
    if (source.line) {
      this.data.source.line = source.line;
    }

    // Allow chaining.
    return this;
  }

  /**
   * Gets the name of the file where the section was originally found.
   *
   * @returns {string} Returns the source file's path relative to the base path.
   */
  sourceFileName() {
    return this.data.source.filename;
  }

  /**
   * Gets the line number where the section was found in the original source file.
   *
   * @returns {string} Returns the source file's line number.
   */
  sourceLine() {
    return this.data.source.line;
  }

  /**
   * Gets or adds nested objects of the section.
   *
   * A common helper for `.modifiers()` and `.parameters()` methods.
   *
   * Different types of arguments for `properties` will yield different results:
   * - `Object|Array`: If the value is an array of objects or an object, the
   *   `properties` are added to this section.
   * - `undefined`: Pass nothing to return all of the section's properties in an
   *   array.
   * - `integer`: Use a 0-based index to return the section's Nth property.
   * - `string`: Use a string to return a specific modifier by name.
   *
   * @private
   * @param {string} propertyName The name of property in `KssSection`.
   * @param {Constructor} objectConstructor The constructor function for the type
   *   of object the property is.
   * @param {*} [properties] Optional. The properties to set for the section.
   * @returns {*} If `properties` is given, the `KssSection` object is returned to
   *   allow chaining of methods. Otherwise, the requested properties of the
   *   section are returned.
   */
  _propertyHelper(propertyName, objectConstructor, properties) {
    if (typeof properties === 'undefined') {
      return this.data[propertyName];
    }

    // If we are given an object, assign the properties.
    if (typeof properties === 'object') {
      if (!(properties instanceof Array)) {
        properties = [properties];
      }
      properties.forEach(property => {
        let newProperty = (property instanceof objectConstructor) ? property : new objectConstructor(property);
        newProperty.section(this);
        this.data[propertyName].push(newProperty);
      });
      // Allow chaining.
      return this;
    }

    // Otherwise, we should search for the requested property.
    let query = properties,
      index = parseInt(query);
    if (typeof query === 'number' || typeof query === 'string' && query === (index + '')) {
      return (index < this.data[propertyName].length) ? this.data[propertyName][index] : false;
      // If the query can be converted to an integer, search by index instead.
    } else {
      // Otherwise, search for the property by name.
      for (let i = 0; i < this.data[propertyName].length; i++) {
        if (this.data[propertyName][i].name() === query) {
          return this.data[propertyName][i];
        }
      }
    }

    // No matching property found.
    return false;
  }

  /**
   * Gets or adds modifiers of the section.
   *
   * Different types of arguments will yield different results:
   * - `modifiers(Object|Array)`: If the value is an array of objects or an
   *   object, the `modifiers` are added to this section.
   * - `modifiers()`: Pass nothing to return all of the section's modifiers in an
   *   array.
   * - `modifiers(Integer)`: Use a 0-based index to return the section's Nth
   *   modifier.
   * - `modifiers(String)`: Use a string to return a specific modifier by name.
   *
   * @param {*} [modifiers] Optional. The modifiers of the section.
   * @returns {KssSection|KssModifier|KssModifier[]|boolean} If `modifiers` is
   *   given, the `KssSection` object is returned to allow chaining of methods.
   *   Otherwise, the requested modifiers of the section are returned.
   */
  modifiers(modifiers) {
    return this._propertyHelper('modifiers', KssModifier, modifiers);
  }

  /**
   * Gets or adds parameters if the section is a CSS preprocessor function/mixin.
   *
   * Different types of arguments will yield different results:
   *  - `parameters(Object|Array)`: If the value is an array of objects or an
   *    object, the `parameters` are added to this section.
   * - `parameters()`: Pass nothing to return all of the section's parameters in
   *   an array.
   * - `parameters(Integer)`: Use a 0-based index to return the section's Nth
   *   parameter.
   * - `parameters(String)`: Use a string to return a specific parameter by name.
   *
   * @param {*} [parameters] Optional. The parameters of the section.
   * @returns {KssSection|KssParameter|KssParameter[]|boolean} If `parameters` is
   *   given, the `KssSection` object is returned to allow chaining of methods.
   *   Otherwise, the requested parameters of the section are returned.
   */
  parameters(parameters) {
    return this._propertyHelper('parameters', KssParameter, parameters);
  }
}

module.exports = KssSection;