Source: lib/traverse.js

  1. 'use strict';
  2. /**
  3. * The `kss/lib/traverse` module is normally accessed via the
  4. * [`traverse()`]{@link module:kss.traverse} method of the `kss` module:
  5. * ```
  6. * const kss = require('kss');
  7. * let styleGuide = kss.traverse(directory, options);
  8. * ```
  9. * @private
  10. * @module kss/lib/traverse
  11. */
  12. const parse = require('./parse.js'),
  13. path = require('path'),
  14. Promise = require('bluebird');
  15. const fs = Promise.promisifyAll(require('fs-extra'));
  16. /**
  17. * Traverse a directory, parse its contents, and create a KssStyleGuide.
  18. *
  19. * Callbacks receive an instance of `KssStyleGuide`.
  20. *
  21. * If you want to parse anything other than css, less, sass, or stylus files
  22. * then you'll want to use options.mask to target a different set of file
  23. * extensions.
  24. *
  25. * ```
  26. * kss.traverse('./stylesheets', { mask: '*.css' }).then(function(styleGuide) {
  27. * styleGuide.sections('2.1.1') // <KssSection>
  28. * });
  29. * ```
  30. *
  31. * There a few extra `options` you can pass to `kss.traverse` which will effect
  32. * the output:
  33. *
  34. * - mask: Use a regex or string (e.g. `*.less|*.css`) to only parse files
  35. * matching this value. Defaults to:
  36. * `*.css|*.less|*.sass|*.scss|*.styl|*.stylus`
  37. * - markdown: kss-node supports built-in Markdown formatting of its
  38. * documentation, thanks to [markdown-it](https://markdown-it.github.io/). It's
  39. * enabled by default, but you can disable it by adding `markdown: false` to
  40. * the `options` object.
  41. * - header: kss-node makes the header available separately from the
  42. * description. To make kss-node behave like the Ruby KSS, disable this option
  43. * and the title will remain a part of the description. This setting is
  44. * enabled by default, but you can disable it by adding `header: false` to
  45. * your options.
  46. *
  47. * @alias module:kss.traverse
  48. * @param {String|Array} directories The directories to traverse
  49. * @param {Object} [options] Options to alter the output content (optional)
  50. * @returns {Promise} A `Promise` object resolving to a `KssStyleGuide`.
  51. */
  52. let traverse = function(directories, options) {
  53. options = options || {};
  54. // Mask to search for particular file types - defaults to common precompilers.
  55. options.mask = options.mask || /\.css|\.less|\.sass|\.scss|\.styl|\.stylus/;
  56. // If the mask is a string, convert it into a RegExp.
  57. if (!(options.mask instanceof RegExp)) {
  58. options.mask = new RegExp(
  59. '(?:' + options.mask.replace(/\./g, '\\.').replace(/\*/g, '.*') + ')$'
  60. );
  61. }
  62. if (!Array.isArray(directories)) {
  63. directories = [directories];
  64. }
  65. let walk = function(directory) {
  66. // Look at the contents of the directory.
  67. return fs.readdirAsync(directory).then(relnames => {
  68. // If there are no files/folders, declare success.
  69. if (relnames.length === 0) {
  70. return Promise.resolve([]);
  71. }
  72. // Otherwise, loop through directory contents.
  73. return Promise.all(
  74. relnames.map(fileName => {
  75. let name = path.join(directory, fileName);
  76. // Check if the directory item is a directory or file.
  77. return fs.statAsync(name).then(stat => {
  78. // Recursively search any directories.
  79. if (stat.isDirectory()) {
  80. if (fileName !== '.svn' && fileName !== '.git') {
  81. return walk(name);
  82. }
  83. // If the file matches our mask, save its path.
  84. } else if (!options.mask || name.match(options.mask)) {
  85. return name;
  86. }
  87. return false;
  88. });
  89. })
  90. ).then(results => {
  91. // Flatten nested array result into a single array.
  92. let files = [];
  93. for (let result of results) {
  94. if (Array.isArray(result)) {
  95. for (let value of result) {
  96. files.push(value);
  97. }
  98. } else if (result) {
  99. files.push(result);
  100. }
  101. }
  102. return files;
  103. });
  104. });
  105. };
  106. // Get each file in the target directory, order them alphabetically and then
  107. // parse their output.
  108. // Loop through all the given directories.
  109. return Promise.all(
  110. directories.map(directory => {
  111. // Normalize the directory path and then "walk" it, collecting file names
  112. // in the fileNames variable.
  113. return walk(path.normalize(directory)).then(files => {
  114. return {
  115. base: directory,
  116. files: files
  117. };
  118. });
  119. })
  120. ).then(results => {
  121. // Flatten nested array result into a single array.
  122. let files = [];
  123. for (let directory of results) {
  124. for (let file of directory.files) {
  125. files.push({
  126. base: directory.base,
  127. path: file
  128. });
  129. }
  130. }
  131. return files;
  132. }).then(files => {
  133. // Read the contents of all the found file names.
  134. return Promise.all(
  135. files.map(file => {
  136. return fs.readFileAsync(file.path, 'utf8').then(contents => {
  137. file.contents = contents;
  138. return file;
  139. });
  140. })
  141. );
  142. }).then(files => {
  143. return parse(files, options);
  144. });
  145. };
  146. module.exports = traverse;