'use strict';
/**
* The `kss/lib/traverse` module is normally accessed via the
* [`traverse()`]{@link module:kss.traverse} method of the `kss` module:
* ```
* const kss = require('kss');
* let styleGuide = kss.traverse(directory, options);
* ```
* @private
* @module kss/lib/traverse
*/
const parse = require('./parse.js'),
path = require('path'),
Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs-extra'));
/**
* Traverse a directory, parse its contents, and create a KssStyleGuide.
*
* Callbacks receive an instance of `KssStyleGuide`.
*
* If you want to parse anything other than css, less, sass, or stylus files
* then you'll want to use options.mask to target a different set of file
* extensions.
*
* ```
* kss.traverse('./stylesheets', { mask: '*.css' }).then(function(styleGuide) {
* styleGuide.sections('2.1.1') // <KssSection>
* });
* ```
*
* There a few extra `options` you can pass to `kss.traverse` which will effect
* the output:
*
* - mask: Use a regex or string (e.g. `*.less|*.css`) to only parse files
* matching this value. Defaults to:
* `*.css|*.less|*.sass|*.scss|*.styl|*.stylus`
* - markdown: kss-node supports built-in Markdown formatting of its
* documentation, thanks to [markdown-it](https://markdown-it.github.io/). It's
* enabled by default, but you can disable it by adding `markdown: false` to
* the `options` object.
* - header: kss-node makes the header available separately from the
* description. To make kss-node behave like the Ruby KSS, disable this option
* and the title will remain a part of the description. This setting is
* enabled by default, but you can disable it by adding `header: false` to
* your options.
*
* @alias module:kss.traverse
* @param {String|Array} directories The directories to traverse
* @param {Object} [options] Options to alter the output content (optional)
* @returns {Promise} A `Promise` object resolving to a `KssStyleGuide`.
*/
let traverse = function(directories, options) {
options = options || {};
// Mask to search for particular file types - defaults to common precompilers.
options.mask = options.mask || /\.css|\.less|\.sass|\.scss|\.styl|\.stylus/;
// If the mask is a string, convert it into a RegExp.
if (!(options.mask instanceof RegExp)) {
options.mask = new RegExp(
'(?:' + options.mask.replace(/\./g, '\\.').replace(/\*/g, '.*') + ')$'
);
}
if (!Array.isArray(directories)) {
directories = [directories];
}
let walk = function(directory) {
// Look at the contents of the directory.
return fs.readdirAsync(directory).then(relnames => {
// If there are no files/folders, declare success.
if (relnames.length === 0) {
return Promise.resolve([]);
}
// Otherwise, loop through directory contents.
return Promise.all(
relnames.map(fileName => {
let name = path.join(directory, fileName);
// Check if the directory item is a directory or file.
return fs.statAsync(name).then(stat => {
// Recursively search any directories.
if (stat.isDirectory()) {
if (fileName !== '.svn' && fileName !== '.git') {
return walk(name);
}
// If the file matches our mask, save its path.
} else if (!options.mask || name.match(options.mask)) {
return name;
}
return false;
});
})
).then(results => {
// Flatten nested array result into a single array.
let files = [];
for (let result of results) {
if (Array.isArray(result)) {
for (let value of result) {
files.push(value);
}
} else if (result) {
files.push(result);
}
}
return files;
});
});
};
// Get each file in the target directory, order them alphabetically and then
// parse their output.
// Loop through all the given directories.
return Promise.all(
directories.map(directory => {
// Normalize the directory path and then "walk" it, collecting file names
// in the fileNames variable.
return walk(path.normalize(directory)).then(files => {
return {
base: directory,
files: files
};
});
})
).then(results => {
// Flatten nested array result into a single array.
let files = [];
for (let directory of results) {
for (let file of directory.files) {
files.push({
base: directory.base,
path: file
});
}
}
return files;
}).then(files => {
// Read the contents of all the found file names.
return Promise.all(
files.map(file => {
return fs.readFileAsync(file.path, 'utf8').then(contents => {
file.contents = contents;
return file;
});
})
);
}).then(files => {
return parse(files, options);
});
};
module.exports = traverse;