Source: kss_styleguide.js

/**
 * 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, sectionQueryMatch;

/**
 * An instance of this class is returned on finishing `kss.traverse`.
 * Exposes convenience methods for interpreting data.
 *
 * @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.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, autoIncrement = [0], 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) {
			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.replace(/\s+\-\s+/g, '.').split('.');
			// If they are already equal, we don't need to increment the section number.
			if (previousRef.join('.') != ref.join('.')) {
				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,
		sections = [];

	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)) {
		query = new RegExp(
			query
				// Convert '*' to a simple .+ regex.
				.replace(/^\*$/, '.+')
				// Convert 'x' to a regex matching one level of reference.
				.replace(/^x$/, '[^\\.\\-]+')
				// Convert '.*' or ' - *' to a simple .* regex.
				.replace(/(\.|\s+\-\s+)\*$/g, '.*')
				// Convert the first '.x' or ' - x' to a regex matching one sub-level
				// of a reference.
				.replace(/(\.|\s+\-\s+)x\b/, '(\\.|\\s+\\-\\s+)[^\\.\\-]+')
				// Convert any remaining '.x' or ' - x' to a regex matching zero or one
				// sub-levels of a reference.
				.replace(/(\.|\s+\-\s+)x\b/g, '((\\.|\\s+\\-\\s+)[^\\.\\-]+){0,1}')
		);
	}

	// General (regex) search
	for (i = 0; i < l; i += 1) {
		current = this.data.sections[i];
		if (sectionQueryMatch(current.data.reference, query)) {
			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 thisStyleguide = 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(/(?:\.|\s\-\s)/),
			refsB = b.data.reference.toLowerCase().split(/(?:\.|\s\-\s)/),
			rLengthA = refsA.length,
			rLengthB = refsB.length,
			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 = thisStyleguide.getWeight(a.reference(), i);
					weightB = thisStyleguide.getWeight(b.reference(), i);
					if (weightA != weightB) {
						return weightA - weightB;
					}
					// If both chunks are digits, use numeric sorting.
					else if (refsA[i].match(/^\d+$/) && refsB[i].match(/^\d+$/)) {
						return refsA[i] - refsB[i];
					}
					// Otherwise, use alphabetical string sorting.
					else {
						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} index The requested depth.
 * @returns {int} The requested weight.
 */
KssStyleguide.prototype.getWeight = function(reference, index) {
	// Initialize the map of weights.
	if (this.data.weightMap === false) {
		this.data.weightMap = {};
		for (var key in this.data.sections) {
			this.data.weightMap[this.data.sections[key].reference().toLowerCase().replace(/\s+\-\s+/g, '.')] = this.data.sections[key].weight();
		}
	}

	reference = reference.toLowerCase().replace(/\s+\-\s+/g, '.');
	if (typeof index !== undefined) {
		reference = reference.split('.', index+1).join('.');
	}

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

sectionQueryMatch = function(reference, query) {
	var match = reference.match(query);
	return match && match[0] === reference;
};

module.exports = KssStyleguide;