'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;