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,