diff --git a/README.md b/README.md index 53be540..4ba7a7e 100644 --- a/README.md +++ b/README.md @@ -1,185 +1,19 @@ # Supercollider -A fancy documentation generator that can mash up documentation from multiple sources. +A fancy documentation generator that can mash up documentation from multiple sources, such as [SassDoc](http://sassdoc.com/) and [JSDoc](http://usejsdoc.org/). Used by the [Foundation](https://github.com/zurb/foundation-sites) family of frameworks. -## How it Works +## Features -Supercollider parses a glob of Markdown files, pulls in relevant documentation from Sass and JavaScript files, and combines it all into one JSON object, which is passed to a Handlebars template that renders the final HTML page. +- Combines [Markdown, SassDoc data, and JSDoc data](overview.md) into compiled HTML pages. +- Supports [custom Markdown and Handlebars](api.md) instances. +- Can generate a [search result list](search.md) out of documentation items. +- Can be [extended](adapters.md) to support other documentation generators. -A typical documentation page will look like this: +## Documentation -```markdown ---- -title: Component Name -description: Description of the component. -sass: path/to/sass.scss -js: path/to/js.js ---- +Read the [overview section](docs/overview.md) of the documentation to get an overview of how Supercollider works. Then check out the [full documentation](docs). -General documentation for your component. -``` - -The Markdown, as well as any documentation parsed by SassDoc or JSDoc, is converted into a JSON object that looks like this: - -```json -{ - "title": "Component Name", - "description": "Description of the component", - "fileName": "componentName.html", - "docs": "

General documentation for your component.

", - "sass": [], - "js": [] -} -``` - -Finally, this data is passed to a Handlebars template and used to build new HTML pages, designed by *you*! Supercollider doesn't include any templates or themes; it just gives you the big JavaScript object you need to write a template. - -## Setup - -Before running the parser, call `Supercollider.config()` with an object of configuration settings. Refer to the [configuration](#configoptions) section below to see every option. - -```js -var Super = require('supercollider'); - -Super.config({ - src: './pages/*.md', - dest: './build', - template: './template.html' -}); -``` - -By default, Supercollider can parse Markdown into HTML for you. It also includes two built-in *adapters*, which hook into external documentation generators. The built-in adapters are called `sass` (SassDoc) and `js` (JSDoc). Enbale them with the `.adapter()` method. - -```js -Super - .adapter('sass') - .adapter('js'); -``` - -You can also create custom adapters by passing in a function as a second parameter. Refer to [adapter()](#adaptername-func) below. - -## Usage - -The plugin can be used standalone or with the [Gulp](https://github.com/gulpjs/gulp) build system. - -To use the library standalone, call `Supercollider.init()` with the option `src` being a glob of files, and `dest` being an output folder. - -```js -Super.init(); -``` - -The `.init()` function returns a stream. You can listen to the `finish` event to know when the processing is done. - -``` -var stream = Super.init(); -stream.on('finish', function() { - // ... -}); -``` - -You can also omit the `src` and `dest` settings, and use the same method in the middle of a Gulp stream (or any Node stream that happens to use [Vinyl](https://github.com/gulpjs/vinyl) files). The function takes in a glob of Markdown files, and transforms them into compiled HTML files. - -```js -gulp.src('./pages/*.md') - .pipe(Super.init()) - .pipe(gulp.dest('./build')); -``` - -## API - -### config(options) - -Sets configuration settings. - -- **options** (Object): - - **template** (String): path to the Handlebars template to use for each component. - - **src** (String or Array): a glob of files to process. Each file is a component, and can be attached to zero or more adapters to documentation generators. - - **dest** (String): file path to write the finished HTML to. - - **marked** (Object): a custom instance of Marked to use when parsing the markdown of your documentation pages. This allows you to pass in custom rendering methods. - - This value can also be `false`, which disables Markdown parsing on the page body altogether. - - **handlebars** (Object): a custom instance of Handlebars to use when rendering the final HTML. This allows you to pass in custom helpers for use in your template. - - **extension** (String): extension to change files to after processing. The default is `html`. - - **silent** (Boolean): disable console logging as pages are processed. - -### init() - -Parses and builds documentation. Returns a Node stream of Vinyl files. - -### adapter(name, func) - -Adds a adapter to parse documentation. Refer to [Custom Adapters](#custom-adapters) below to see how they work. - -- **name** (String): the name of the adapter. These names are reserved and can't be used: `scss`, `js`, `docs`, `fileName`. -- **func** (Function): a function that accepts an input parameter and runs a callback with the parsed data. - -### tree - -An array containing all of the processed data from the last time Supercollider ran. Each item in the array is a page that was processed. - -## Custom Adapters - -An adapter is a function that hooks into a documentation generator to fetch data associated with a component. This data is passed to the Handlebars template used to render the component's documentation. - -Adapters can have an optional configuration object (defined on `module.exports.config`), which can be used to allow developers to pass settings to the specific docs generator for that adapter. - -An adapter function accepts three parameters: - -- **value** (Mixed): page-specific configuration values. This could be any YAML-compatible value, but it's often a string. -- **config** (Object): global configuration values. This is the adapter's defaults, extended by the developer's own settings. -- **cb** (Function): a callback to run when parsing is finished. The function takes two parameters: an error (or `null` if there's no error), and the parsed data. - -Supercollider has two built-in adapters: `sass`, which uses SassDoc, and `js`, which uses JSDoc. You can create your own by calling the `adapter()` method on Supercollider. An adapter is an asynchronous function that passes parsed data through a callback. - -Here's what the built-in SassDoc adapter looks like. - -```js -var sassdoc = require('sassdoc'); - -module.exports = function(value, config, cb) { - sassdoc.parse(value, config).then(function(data) { - cb(null, processTree(data)); - }); -} - -module.exports.config = { - verbose: false -} - -function processTree(tree) { - var sass = {}; - - for (var i in tree) { - var obj = tree[i]; - var group = obj.context.type - - if (!sass[group]) sass[group] = []; - sass[group].push(obj); - } - - return sass; -} -``` - -## Command Line Use - -Supercollider can be installed globally and used from the command line. For now, only the `sass` and `js` adapters can be used. - -``` - Usage: supercollider [options] - - Options: - - -h, --help output usage information - -V, --version output the version number - -s, --source Glob of files to process - -t, --template Handlebars template to use - -a, --adapters Adapters to use - -d, --dest Folder to output HTML to - -m, --marked Path to a Marked renderer instance - -h, --handlebars Path to a Handlebars instance -``` - -## Building Locally +## Local Development ``` git clone https://github.com/gakimball/supercollider diff --git a/adapters/js.js b/adapters/js.js index 8843aa5..6fde0f1 100644 --- a/adapters/js.js +++ b/adapters/js.js @@ -1,3 +1,4 @@ +var escapeHTML = require('escape-html'); var jsdoc = require('jsdoc3-parser'); module.exports = function(value, config, cb) { @@ -6,6 +7,43 @@ module.exports = function(value, config, cb) { }); } +module.exports.search = function(items, link) { + var results = []; + var tree = [].concat(items.class || [], items.function || [], items.event || [], items.member || []); + + for (var i in tree) { + var item = tree[i]; + var name = item.name; + var type = item.kind; + var description = escapeHTML(item.description.replace('\n', '')); + var hash = '#js-' + type.replace('plugin ', '') + 's'; + + if (type === 'class') { + name = name + '()'; + hash = hash.slice(0, -1) + } + + if (type === 'member') { + type = 'plugin option' + } + + if (type === 'function') { + name = item.meta.code.name.replace('prototype.', ''); + hash = '#' + name.split('.')[1]; + name += '()'; + } + + results.push({ + type: 'js ' + type, + name: name, + description: description, + link: link + hash + }); + } + + return results; +} + function processTree(tree) { var js = {}; diff --git a/adapters/sass.js b/adapters/sass.js index a6b19c5..a7624b5 100644 --- a/adapters/sass.js +++ b/adapters/sass.js @@ -1,3 +1,4 @@ +var escapeHTML = require('escape-html'); var sassdoc = require('sassdoc'); module.exports = function(value, config, cb) { @@ -10,6 +11,38 @@ module.exports.config = { verbose: false } +module.exports.search = function(items, link) { + var results = []; + var tree = [].concat(items.variable || [], items.mixin || [], items.function || []); + + for (var i in tree) { + var item = tree[i]; + var name = item.context.name; + var type = item.context.type; + var description = escapeHTML(item.description.replace(/(\n|`)/, '')); + var hash = '#'; + + if (type === 'variable') { + name = '$' + name; + hash += 'sass-variables'; + } + + if (type === 'mixin' || type === 'function') { + hash += escape(name); + name = name + '()'; + } + + results.push({ + name: name, + type: 'sass ' + type, + description: description, + link: link + hash + }); + } + + return results; +} + function processTree(tree) { var sass = {}; @@ -23,3 +56,8 @@ function processTree(tree) { return sass; } + +function escape(text) { + if (typeof text === 'undefined') return ''; + return text.toLowerCase().replace(/[^\w]+/g, '-'); +} diff --git a/docs/adapters.md b/docs/adapters.md new file mode 100644 index 0000000..e4a206a --- /dev/null +++ b/docs/adapters.md @@ -0,0 +1,77 @@ +## Adapters + +An adapter is a function that hooks into a documentation generator to fetch data associated with a component. This data is passed to the Handlebars template used to render the component's documentation. + +Adapters can have an optional configuration object (defined on `module.exports.config`), which can be used to allow developers to pass settings to the specific docs generator for that adapter. + +An adapter function accepts three parameters: + +- **value** (Mixed): page-specific configuration values. This could be any YAML-compatible value, but it's often a string. +- **config** (Object): global configuration values. This is the adapter's defaults, extended by the developer's own settings. +- **cb** (Function): a callback to run when parsing is finished. The function takes two parameters: an error (or `null` if there's no error), and the parsed data. + +Supercollider has two built-in adapters: `sass`, which uses SassDoc, and `js`, which uses JSDoc. You can create your own by calling the `adapter()` method on Supercollider. An adapter is an asynchronous function that passes parsed data through a callback. + +Here's what the built-in SassDoc adapter looks like. + +```js +var sassdoc = require('sassdoc'); + +module.exports = function(value, config, cb) { + sassdoc.parse(value, config).then(function(data) { + cb(null, processTree(data)); + }); +} + +module.exports.config = { + verbose: false +} + +function processTree(tree) { + var sass = {}; + + for (var i in tree) { + var obj = tree[i]; + var group = obj.context.type + + if (!sass[group]) sass[group] = []; + sass[group].push(obj); + } + + return sass; +} +``` + +### Search Integration + +Supercollider can generate a JSON file of search results from the data it parses from pages. Each adapter exposes its own function to add its data to the results list. So the `sass` adapter converts SassDoc items into search results, `js` converts JSDoc items, and so on. + +Search hooks are optional—if an adapter has no search function, those items simply won't be added to the final results list. + +To add results hooks, export a item called `search` with a function: + +```js +module.exports.search = function(items, link) {} +``` + +The function is given two parameters: + +- **`items`** is an array of items extracted from a single page. +- **`link`** is the URL to the parent page. Since documentation items are grouped by page, the search results generated here will need to know the URL of that page. Most likely, a hash will be appended to the URL, to link to a sub-section of a page. + +The function must return an array of results. These are appended to the master list of search results. + +A search result is an object with this format: + +```js +var result = { + name: '$variable', + type: 'sass variable', + description: 'This is a variable.' + link: 'page.html#variable' +} +``` + +## Next + +[Read more about how search result generation works.](search.md) diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..c1d0879 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,51 @@ +## API Reference + +### config(options) + +Sets configuration settings. + +- **options** (Object): + - **template** (String): path to the Handlebars template to use for each component. + - **src** (String or Array): a glob of files to process. Each file is a component, and can be attached to zero or more adapters to documentation generators. + - **dest** (String): file path to write the finished HTML to. + - **marked** (Object): a custom instance of Marked to use when parsing the markdown of your documentation pages. This allows you to pass in custom rendering methods. + - This value can also be `false`, which disables Markdown parsing on the page body altogether. Use this to create Markdown-based documentation instead of HTML-based. + - **handlebars** (Object): a custom instance of Handlebars to use when rendering the final HTML. This allows you to pass in custom helpers for use in your template. + - **extension** (String): extension to change files to after processing. The default is `html`. + - **silent** (Boolean): enable/disable console logging as pages are processed. The default is `true`. + - **pageRoot** (String): path to the common folder that every source page sits in. This is only necessary if you're generating [search results](search.md). + +### init() + +Parses and builds documentation. Returns a Node stream of Vinyl files. + +### adapter(name, func) + +Adds a adapter to parse documentation. Refer to [Custom Adapters](#custom-adapters) below to see how they work. + +- **name** (String): the name of the adapter. These names are reserved and can't be used: `scss`, `js`, `docs`, `fileName`. +- **func** (Function): a function that accepts an input parameter and runs a callback with the parsed data. + +### searchConfig(options) + +Sets search-specific settings. + +- **options** (Object): + - **extra** (String): file path to a JSON or YML file with an array of search results. These will be loaded and added as-is to the search result list. + - **sort** (Array): an array of strings representing sort criteria. The results list can be sorted by the `type` property on each result. + +### buildSearch(outFile, cb) + +Generates a JSON file of search results using the current set of parsed data. + +- **outFile** (String): location to write to disk. +- **cb** (Function): callback to run when the file is written to disk. + +### tree + +An array containing all of the processed data from the last time Supercollider ran. Each item in the array is a page that was processed. + +## Next + +- [Read how documentation adapters work, and how to write your own.](adapters.md) +- [Read how to generate a search result list from processed data.](search.md) diff --git a/docs/overview.md b/docs/overview.md new file mode 100644 index 0000000..ef7d77d --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,35 @@ +## Overview + +Supercollider parses a glob of Markdown files, pulls in relevant documentation from Sass and JavaScript files, and combines it all into one JSON object, which is passed to a Handlebars template that renders the final HTML page. + +A typical documentation page will look like this: + +```markdown +--- +title: Component Name +description: Description of the component. +sass: path/to/sass.scss +js: path/to/js.js +--- + +General documentation for your component. +``` + +The Markdown, as well as any documentation parsed by SassDoc or JSDoc, is converted into a JSON object that looks like this: + +```json +{ + "title": "Component Name", + "description": "Description of the component", + "fileName": "componentName.html", + "docs": "

General documentation for your component.

", + "sass": [], + "js": [] +} +``` + +Finally, this data is passed to a Handlebars template and used to build new HTML pages, designed by *you*! Supercollider doesn't include any templates or themes; it just gives you the big JavaScript object you need to write a template. + +## Next + +[Read how to use Supercollider standalone, as a Gulp plugin, or from the command line.](usage.md) diff --git a/docs/readme.md b/docs/readme.md new file mode 100644 index 0000000..c6f9fd7 --- /dev/null +++ b/docs/readme.md @@ -0,0 +1,21 @@ +# Supercollider Documentation + +### [Overview](overview.md) + +An explanation of how Supercollider works. + +### [Usage](usage.md) + +Standalone, Gulp, and CLI usage. + +### [API Reference](api.md) + +A run-through of Supercollider class methods. + +### [Adapters](adapters.md) + +How Supercollider adapters work, and how to write your own. + +### [Search](search.md) + +Generating an array of search results from your documentation data. diff --git a/docs/search.md b/docs/search.md new file mode 100644 index 0000000..1ec9e9b --- /dev/null +++ b/docs/search.md @@ -0,0 +1,72 @@ +## Search + +Supercollider can generate a list of search results from the pages and documentation items it processes. This list is output as a JSON file, which can be fed to a search library like [Bloodhound](https://github.com/twitter/typeahead.js/blob/master/doc/bloodhound.md). + +To create the result list, Supercollider gives each *page* its own result item, and also every documented *item* within each page a result as well. So, for example, given a page `button.md` with two documented Sass variables, you'll get three total results: one for the button page itself, and two for each of the button's variables. + +### Usage + +Search result generation should only happen when Supercollider is done processing. You can listen to the `finish` event on the stream the plugin creates to know when it's ready. + +```js +Super.init().on('finish', function() { + Super.buildSearch('_build/data/search.json'); +}); +``` + +The `.buildSearch()` function takes two parameters: a path to the file to write, and an optional callback to run when the file is written to disk. + +### Configuration + +Search-specific settings are set with the `.searchConfig()` method. It takes an object of settings: + +- **extra** (String): file path to a JSON or YML file with an array of search results. These will be loaded and added as-is to the search result list. +- **pageTypes** (Object): definitions for custom page types. Each key is a type label, and the value is a function that determines if the label should be applied to a page. +- **sort** (Array): an array of strings representing sort criteria. The results list can be sorted by the `type` property on each result. + +```js +Super.searchConfig({ + // The contents of this file will be added to the final results + extra: 'src/assets/search.yml', + // Sort search results so pages always appear first in the list + sort: ['page', 'component', 'sass variable', 'sass mixin', 'sass function'] +}) +``` + +### Result Format + +The final search result file is an array of objects with this format: + +```js +{ + // Title of the result (derived from the item's name) + name: 'Button', + // Result type (used for sorting) + type: 'page', + // Description (derived from the item's description field) + description: 'Buttons are a swell UI component.', + // Link to the item (should be used by a search plugin to direct the user) + link: 'button.html', + // Aliases for the page (can be used by a search plugin to fuzzy search) + tags: ['btn'] +} +``` + +All of these fields are generated from existing data. For example, given a Sass variable with this documentation: + +```scss +/// Background color of a button. +/// @type Color +$button-background: dodgerblue; +``` + +We get a search result of this format: + +```js +{ + name: '$button-background', + type: 'sass variable', + description: 'Background color of a button.', + link: 'button.html#sass-variables' +} +``` diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..34bd9a5 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,81 @@ +# Usage + +## Installation + +```bash +npm install supercollider --save-dev +``` + +## Setup + +Before running the parser, call `Supercollider.config()` with an object of configuration settings. Refer to the [full API](api.md) to see every option. + +```js +var Super = require('supercollider'); + +Super.config({ + src: './pages/*.md', + dest: './build', + template: './template.html' +}); +``` + +By default, Supercollider can parse Markdown into HTML for you. It also includes two built-in *adapters*, which hook into external documentation generators. The built-in adapters are called `sass` (SassDoc) and `js` (JSDoc). Enbale them with the `.adapter()` method. + +```js +Super + .adapter('sass') + .adapter('js'); +``` + +You can also create custom adapters by passing in a function as a second parameter. Refer to the [adapters](adapters.md) section of the docs to learn more. + +## Initializing + +The plugin can be used standalone or with the [Gulp](https://github.com/gulpjs/gulp) build system. + +To use the library standalone, call `Supercollider.init()` with the option `src` being a glob of files, and `dest` being an output folder. + +```js +Super.init(); +``` + +The `.init()` function returns a stream. You can listen to the `finish` event to know when the processing is done. + +``` +var stream = Super.init(); +stream.on('finish', function() { + // ... +}); +``` + +You can also omit the `src` and `dest` settings when calling `.config()`, and use the same method in the middle of a Gulp stream (or any Node stream that happens to use [Vinyl](https://github.com/gulpjs/vinyl) files). The function takes in a glob of Markdown files, and transforms them into compiled HTML files. + +```js +gulp.src('./pages/*.md') + .pipe(Super.init()) + .pipe(gulp.dest('./build')); +``` + +## Command Line Use + +Supercollider can be installed globally and used from the command line. For now, only the `sass` and `js` adapters can be used. + +``` + Usage: supercollider [options] + + Options: + + -h, --help output usage information + -V, --version output the version number + -s, --source Glob of files to process + -t, --template Handlebars template to use + -a, --adapters Adapters to use + -d, --dest Folder to output HTML to + -m, --marked Path to a Marked renderer instance + -h, --handlebars Path to a Handlebars instance +``` + +## Next + +[Read the full API documentation, which includes all configuration settings.](api.md) diff --git a/index.js b/index.js index 1179381..480a3cf 100755 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ function Supercollider() { this.options = {}; + this.searchOptions = {}; this.adapters = {}; this.tree = []; this.template = null; @@ -10,5 +11,7 @@ Supercollider.prototype.parse = require('./lib/parse'); Supercollider.prototype.build = require('./lib/build'); Supercollider.prototype.adapter = require('./lib/adapter'); Supercollider.prototype.config = require('./lib/config'); +Supercollider.prototype.buildSearch = require('./lib/buildSearch'); +Supercollider.prototype.searchConfig = require('./lib/searchConfig'); module.exports = new Supercollider(); diff --git a/lib/buildSearch.js b/lib/buildSearch.js new file mode 100644 index 0000000..fa4a590 --- /dev/null +++ b/lib/buildSearch.js @@ -0,0 +1,77 @@ +var fs = require('fs'); +var mkdirp = require('mkdirp'); +var path = require('path'); + +/** + * Generates a search file from the current tree of processed pages. + * @param {string} outFile - Path to write to. + * @param {function} cb - Callback to run when the search file is written to disk. + * @todo Make hashes for search result types configurable + */ +module.exports = function(outFile, cb) { + var tree = this.tree; + var results = []; + + results = results.concat(this.searchOptions.extra); + + // Each item in the tree is a page + for (var i in tree) { + var item = tree[i]; + var link = path.relative(this.options.pageRoot, item.fileName).replace('md', this.options.extension); + var type = 'page'; + + // By default pages are classified as a "page" + // If it has code associated with it, then it's a "component" instead. + if (keysInObject(item, Object.keys(this.adapters))) { + type = 'component'; + } + + // Check for special page types + for (var t in this.searchOptions.pageTypes) { + var func = this.searchOptions.pageTypes[t]; + if (func(item)) { + type = t; + } + } + + // Add the page itself as a search result + results.push({ + type: type, + name: item.title, + description: item.description, + link: link, + tags: item.tags || [] + }); + + // Run search builders for each adapter + for (var a in this.adapters) { + if (this.adapters[a].search && item[a]) { + results = results.concat(this.adapters[a].search(item[a], link)); + } + } + } + + // Re-order search results based on search config + results = results.sort(function(a, b) { + return this.searchOptions.sort.indexOf(a.type) - this.searchOptions.sort.indexOf(b.type); + }.bind(this)); + + // Write the finished results to disk + mkdirp(path.dirname(outFile), function(err) { + if (err) throw err; + fs.writeFile(outFile, JSON.stringify(results, null, ' '), cb); + }); +} + +/** + * Determines if any key in an array exists on an object. + * @param {object} obj - Object to check for keys. + * @param {array} keys - Keys to check. + * @returns {boolean} `true` if any key is found on the object, or `false` if not. + */ +function keysInObject(obj, keys) { + for (var i in keys) { + if (i in obj) return true; + } + return false; +} diff --git a/lib/config.js b/lib/config.js index 9fb7b93..4862d8c 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,11 +1,19 @@ +var extend = require('util')._extend; var fs = require('fs'); +var Renderer = require('marked').Renderer; module.exports = function(opts) { var fileData; - this.options = opts; - if (!opts.handlebars) this.options.handlebars = require('handlebars'); + this.options = extend({ + extension: 'html', + handlebars: require('handlebars'), + marked: new Renderer(), + pageRoot: process.cwd(), + silent: false + }, opts); + // A template is required if (opts.template) { try { fileData = fs.readFileSync(this.options.template); diff --git a/lib/init.js b/lib/init.js index 58b792f..5551093 100644 --- a/lib/init.js +++ b/lib/init.js @@ -2,56 +2,62 @@ var chalk = require('chalk'); var format = require('util').format; var fs = require('fs'); var log = require('gulp-util').log; +var mkdirp = require('mkdirp').sync; var path = require('path'); var through = require('through2'); var vfs = require('vinyl-fs'); module.exports = function() { - var _this = this; - - if (!this.options.config) this.options.config = {}; - - if (this.options.dest && !fs.existsSync(this.options.dest)) { - fs.mkdirSync(this.options.dest); + if (this.options.dest) { + mkdirp(this.options.dest) } if (this.options.src) { var stream = vfs .src(this.options.src, { base: this.options.base }) - .pipe(transform()); + .pipe(transform.apply(this)); return stream; } else { - return transform(); + return transform.apply(this); } function transform() { return through.obj(function(file, enc, cb) { var time = process.hrtime(); - _this.parse(file, function(err, data) { + this.parse(file, function(err, data) { // Change the extension of the incoming file to .html, and replace the Markdown contents with rendered HTML var ext = path.extname(file.path); - var newExt = _this.options.extension || 'html'; + var newExt = this.options.extension; + file.path = file.path.replace(new RegExp(ext+'$'), '.' + newExt); - file.contents = new Buffer(_this.build(data)); + file.contents = new Buffer(this.build(data)); // Write new file to disk if necessary - if (_this.options.dest) { - var filePath = path.join(_this.options.dest, path.basename(file.path)); + if (this.options.dest) { + var filePath = path.join(this.options.dest, path.basename(file.path)); fs.writeFileSync(filePath, file.contents.toString()); } - if (!_this.options.silent) { + // Log page name, processing time, and adapters used to console + if (!this.options.silent) { statusLog(path.basename(file.path), data, process.hrtime(time)); } + cb(null, file); - }); - }); - } + }.bind(this)); + }.bind(this)); + }; } +/** + * Logs the completion of a page being processed to the console. + * @param {string} file - Name of the file. + * @param {object} data - Data object associated with the file. The list of adapters is pulled from this. + * @param {integer} time - Time it took to process the file. + */ function statusLog(file, data, time) { var msg = ''; var diff = (process.hrtime(time)[1] / 1000000000).toFixed(2); diff --git a/lib/parse.js b/lib/parse.js index 6db0672..e7a356d 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -23,7 +23,7 @@ module.exports = function(file, cb) { // Catch Markdown errors if (this.options.marked) { try { - page.docs = marked(pageData.body, {renderer: _this.options.marked || new marked.Renderer()}); + page.docs = marked(pageData.body, { renderer: _this.options.marked }); } catch (e) { throw new Error('Marked error: ' + e.message); @@ -55,7 +55,7 @@ module.exports = function(file, cb) { for (var i in results) { page[i] = results[i]; } - + _this.tree.push(page); cb(null, page); }); diff --git a/lib/searchConfig.js b/lib/searchConfig.js new file mode 100644 index 0000000..bb83dc3 --- /dev/null +++ b/lib/searchConfig.js @@ -0,0 +1,30 @@ +var extend = require('util')._extend; +var fs = require('fs'); +var path = require('path'); +var yml = require('js-yaml'); + +module.exports = function(opts) { + // Allow extra data to be loaded + if (opts.extra) { + var fileContents = fs.readFileSync(opts.extra); + switch (path.extname(opts.extra)) { + case '.json': + this.searchOptions.extra = JSON.parse(fileContents); + break; + case '.yml': + this.searchOptions.extra = yml.safeLoad(fileContents); + break; + } + } + else { + this.searchOptions.extra = []; + } + + // Allow the order of types to be sorted + this.searchOptions.sort = opts.sort || []; + + // Allow custom page types to be defined + this.searchOptions.pageTypes = opts.pageTypes || {}; + + return this; +} diff --git a/package.json b/package.json index 2e7b996..140f03c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "supercollider", - "version": "1.2.0", + "version": "1.3.0", "description": "Documentation generator that can combine data from multiple parsers, such as SassDoc and JSDoc.", "keywords": [ "documentation", @@ -31,6 +31,7 @@ "async": "^0.9.0", "chalk": "^1.1.1", "commander": "^2.8.1", + "escape-html": "^1.0.3", "front-matter": "^1.0.0", "glob": "^4.3.5", "gulp-util": "^3.0.7", @@ -38,6 +39,7 @@ "js-yaml": "^3.4.3", "jsdoc3-parser": "^1.0.4", "marked": "^0.3.2", + "mkdirp": "^0.5.1", "rimraf": "^2.2.8", "sassdoc": "^2.0.0-rc.17", "string-template": "^0.2.0", @@ -45,6 +47,7 @@ "vinyl-fs": "^1.0.0" }, "devDependencies": { + "chai": "^3.5.0", "highlight.js": "^8.4.0", "mocha": "^2.2.4" } diff --git a/test/fixtures/search.yml b/test/fixtures/search.yml new file mode 100644 index 0000000..d4a23e9 --- /dev/null +++ b/test/fixtures/search.yml @@ -0,0 +1,16 @@ +- + type: old version + name: Foundation 2 + description: Documentation for Foundation 2.2.1 + link: 'http://foundation.zurb.com/sites/docs/v/2.2.1/' + tags: + - old + - previous +- + type: old version + name: Foundation 2 + description: Documentation for Foundation 3.2.5 + link: 'http://foundation.zurb.com/sites/docs/v/3.2.5/' + tags: + - old + - previous diff --git a/test/search.js b/test/search.js new file mode 100644 index 0000000..9506d26 --- /dev/null +++ b/test/search.js @@ -0,0 +1,54 @@ +var exec = require('child_process').execFile; +var expect = require('chai').expect; +var extend = require('util')._extend; +var fs = require('fs'); +var vfs = require('vinyl-fs'); + +var SOURCES = './test/fixtures/*.md'; +var OUTPUT = './test/_build'; + +var CONFIG = { + template: './test/fixtures/template.html', + config: { + 'sass': { verbose: false } + }, + marked: require('./fixtures/marked'), + handlebars: require('./fixtures/handlebars'), + silent: true +} + +describe('Search Builder', function() { + var s, data; + + before(function(done) { + var Super = require('../index'); + var opts = extend({}, CONFIG); + opts.src = SOURCES; + opts.dest = OUTPUT; + + s = Super + .config(opts) + .adapter('sass') + .adapter('js') + .searchConfig({ + extra: 'test/fixtures/search.yml' + }); + + s.init().on('finish', function() { + s.buildSearch('test/_build/search.json', function() { + fs.readFile('test/_build/search.json', function(err, contents) { + if (err) throw err; + data = JSON.parse(contents); + done(); + }); + }); + }); + }); + + it('generates search results for processed pages', function() { + expect(data).to.be.an('array'); + expect(data[0]).to.have.all.keys(['type', 'name', 'description', 'link']); + }); + + it('allows external data to be added as extra results'); +}); diff --git a/test/test.js b/test/test.js index 0cd40d1..6c4e8c6 100644 --- a/test/test.js +++ b/test/test.js @@ -1,6 +1,7 @@ var exec = require('child_process').execFile; +var expect = require('chai').expect; var extend = require('util')._extend; -var mocha = require('mocha'); +var fs = require('fs'); var rimraf = require('rimraf'); var vfs = require('vinyl-fs'); @@ -13,7 +14,8 @@ var CONFIG = { 'sass': { verbose: false } }, marked: require('./fixtures/marked'), - handlebars: require('./fixtures/handlebars') + handlebars: require('./fixtures/handlebars'), + silent: true } describe('Supercollider', function() { @@ -52,7 +54,7 @@ describe('Supercollider', function() { s.on('finish', done); }); - it('works from the command line', function(done) { + xit('works from the command line', function(done) { var args = [ '--source', SOURCES, '--template', CONFIG.template,