LivingCSS
Parse comments in your CSS to generate a living style guide using Markdown, Handlebars, Polymer, and Prism syntax highlighter.
Installation
$ npm install --save livingcss
Demos
See the github page for an example of the default output of LivingCSS.
See FamilySearch.org Style Guide for an example of using LivingCSS with custom tags and a custom Handlebars template. This gist shows the gulpfile and Handlebars template used by the FamilySearch Style Guide.
Gulp
Use gulp-livingcss
var gulp = require('gulp');
var livingcss = require('gulp-livingcss');
gulp.task('default', function () {
gulp.src('src/styles.css')
.pipe(livingcss())
.pipe(gulp.dest('dist'))
});
Usage
var livingcss = require('livingcss');
// livingcss(source, dest, options)
livingcss('input.css', 'styleguide', options);
- source - A file path, glob, or mixed array of both, matching files to be parsed to generate the style guide. Any file type can be used so long as it allows
/** */
type comments. - dest - Directory to output the style guide HTML. Defaults to the current directory.
- options - optional list of options.
How it works
LivingCSS parses JSDoc-like comments for documentation in order to create a living style guide. A documentation comment follows the following format.
/**
* A short description or lengthy explanation about the style. Will be parsed
* using `markdown`.
*
* Descriptions can be multiple lines and end at the first encountered tag.
* Tags can be in any order and can be multiple lines long as well.
*
* @section Section Name
* @example
* <div class="my-awesome-class">Example</div>
*/
What makes LivingCSS different than other tag-like comment parsers is that it does not try to impose a strict tag rule set. Instead, it defines a few basic tags for you to use, but any tag will be parsed so long as it follows the @tag {type} name - description
format (where type, name, and description are all optional).
It also generates a JSON object of the parsed comments that can be used to generate style guides using other templating languages.
Defined tags
-
@tag {type} name - description
- Any tag that follows this format will be parsed. The type, name, and description are all optional. If only thetag
is defined, the description will be set totrue
. Multiple tags with the same name will be added as an array in the context object.-
If the type is
{markdown}
, the description will be parsed as markdown./** * @tag {markdown} *Will* be parsed as `markdown`. */
/** * Example of multiple tags with the same name. Will result in: * * { * color: [ * {name: 'Red', description: '#f00'}, * {name: 'Green', description: '#0f0'}, * {name: 'Blue', description: '#00f'} * ] * } * * @color Red - #f00 * @color Green - #0f0 * @color Blue - #00f */
-
-
@section
- Add a new section to the style guide. The@section
tag can define the name of the section or the first line of the comment description will be used as the section name./** * My Section * * A description of the section and how to use it. * * @section */ /** * A description of the section and how to use it. * * @section My Section */
-
@sectionof
- Add a section as a child of another section. There is no limit to the number of nested sections. If the section you are referencing is a child of another section, then the value of@sectionof
must use all parent section names delimited by a period./** * A description of the parent section. * * @section Parent Section */ /** * A description of the child section. * * @section Child Section * @sectionof Parent Section */ /** * A child of Child Section * * @section Grandchild Section * @sectionof Parent Section.Child Section */ /** * A child of Grandchild Section * * @section Great-grandchild Section * @sectionof Parent Section.Child Section.Grandchild Section */
-
@page
- Add a section to a page. Each unique page will output its own HTML file. Child sections will inherit the page of the parent. Defaults toindex
./** * Section belonging to a page. * * @section Section Name * @page Page Name */
-
@example
- Provide an example that will be displayed in the style guide. Can provide a type to change the language for code highlighting, and you can also provide a file path to be used as the example. See Prisms supported languages for valid types./** * A simple example. * * @section Example * @example * <div>foo</div> */ /** * An example with a language type. * * @section Example * @example {javascript} * console.log('foo'); */ /** * An example from a file * * @section Example * @example * relative/path/to/file.html */
NOTE: By default, the style guide only loads Prism markup (HTML) syntax highlighting. If you need another syntax language (use files for
v1.6.0
), you'll have to add it to thecontext.scripts
array. -
@code
- Same as@example
, but can be used to override the code output to be different than the example output. Useful if you need to provide extra context for the example that does not need to be shown in the code. If you need to use the@
symbol at the start of a newline (such as with@extend
or@include
in CSS preprocessors), use the HTML entity encoding@
, otherwise the parser will try to parse it as a tag./** * Different example output than code output * * @section Code Example * @example * <div class="container"> * <div class="my-awesome-class">Example</div> * </div> * * @code * <div class="my-awesome-class">Example</div> */ /** * Using the @ symbol in code * * @section Code With At Symbol * @code * .example { * @extend %placeholder-selector; * } */
-
@hideCode
- Hide the code output of the example./** * You can only see the example, the code is hidden. * * @section hideCode Example * @example * <div class="container"> * <div class="my-awesome-class">Example</div> * </div> * @hideCode */
-
@doc
- A file that defines the section name, description, example, or code. Useful if your section description needs to output HTML. The first heading of the file will be used as the section description if one is not defined./** * @doc externalDoc.md * @section */
[//]: # "externalDoc.md" # Section Title This markdown file can define the `@section` name using a heading and the section description. Anything that is not an `@example` or `@code` code block will be added to the description of the section. ``` This code block will be part of the description. ``` ```html @example <p>This code block will be treated as the <code>@example</code> tag</p> ``` @code <p>This code block will be treated as the <code>@code</code> tag.</p> <p>You can use either the triple ticks or 4 spaces when defining the <code>@example</code> or <code>@code</code> blocks.</p>
NOTE: Because
@doc
can provide a section name, the@section
tag must come after the@doc
tag, otherwise you will get an Unnamed Section error.
Options
loadcss
- If the style guide should load the css files that were used to generate it. The style guide will not move the styles to the output directory but will merely link to the styles in their current directory (so relative paths from the styles still work). Defaults totrue
.minify
- If the generated HTML should be minified. Defaults tofalse
.preprocess
- Function that will be called for every page right before Handlebars is called with the context object. The function will be passed the context object, the template, and the Handlebars object as parameters. Return false if you don't want the style guide to be generated using Handlebars, or return a Promise if you need to make asynchronous calls (reject the Promise to not use Handlebars). Use this function to modify the context object, add additional styles to the examples, or register Handlebars partials, helpers, or decorators.sortOrder
- List of pages and their sections in the order they should be sorted. Any page or section not listed will be added to the end in the order encountered. Can be an array of page names to just sort pages, an array of objects with page names as keys and an array of section names as values to sort both pages and sections, or a mix of both. Names are case insensitive.tags
- Object of custom tag names to callback functions that are called when the tag is encountered. The tag, the parsed comment, the block object, the list of sections, the list of pages, and the file are passed as thethis
object to the callback function.template
- Path to the Handlebars template to use for generating the HTML. Defaults to the LivingCSS templatetemplate/template.hbs'.
livingcss(['input.css', 'css/*.css'], 'styleguide.html', {
loadcss: true,
minify: true,
preprocess: function(context, template, Handlebars) {
context.title = 'My Awesome Style Guide';
// register a Handlebars partial
Handlebars.registerPartial('myPartial', '{{name}}');
},
sortOrder: [
// sort the pages components and modules in that order, and sort sections
// within those pages.
{
components: ['modals', 'dropdowns']
},
{
modules: ['timeStamp']
},
// sort the pages atoms and molecules after components and modules, but
// not in any particular order between themselves. Also sort sections
// within those pages.
{
atoms: ['buttons', 'forms', 'images'],
molecules: ['cards']
},
// sort the pages organisms and templates in that order, but not any of
// their sections
['organisms', 'templates']
],
tags: {
color: function() {
var section = this.sections[this.tag.description];
if (section) {
section.colors = section.colors || [];
section.colors.push({
name: this.tag.name,
value: this.tag.type
});
}
}
},
template: 'styleguide.hb'
});
Custom Tags
You can create your own tags to modify how the documentation is generated. Because most tags are automatically parsed for you, you shouldn't need to create custom tags very often. To create a custom tag, use the options.tags
option.
A tag is defined as the tag name and a callback function that will be called when the tag is encountered. The current tag, the parsed comment, the block object, the list of sections, the list of pages, and the current file are passed as the this
object to the callback function.
The comment is parsed using comment-parser, and the current tag and the parsed comment will follow the output returned by it. The block object is the current state of the comment, including the comments description, all parsed tags associated with the comment, and any other modifications done by other tags. The block object is also the object saved to the sections
array when a section
tag is used, and the pages
array when a page
tag is used.
For example, if you wanted to generate a color palette for your style guide, you could create a custom tag to add the color to a section.
/**
* @color {#F00} Brand Red - Section Name
*/
livingcss('input.css', 'styleguide.html', {
tags: {
color: function() {
/* this.tag = {
tag: 'color',
type: '#F00',
name: 'Brand Red'
description: 'Section Name',
} */
var section = this.sections[this.tag.description];
if (section) {
section.colors = section.colors || [];
section.colors.push({
name: this.tag.name,
value: this.tag.type
});
}
}
}
});
Context Object
Use the options.preprocess
option to modify or use the context object before it is passed to Handlebars. The function will be passed the context object, the template, and the Handlebars object as parameters. Return false if you don't want the style guide to be generated using Handlebars, or return a Promise if you need to make asynchronous calls (reject the Promise to not use Handlebars). Use this function to modify the context object, add additional styles to the examples, or register Handlebars partials, helpers, or decorators.
livingcss('input.css', 'styleguide.html', {
preprocess: function(context, template, Handlebars) {
context.title = 'My Awesome Style Guide';
// register a Handlebars partial
Handlebars.registerPartial('myPartial', '{{name}}');
}
});
-
allSections
- List of all sections. -
footerHTML
- HTML content of the footer. Defaults toStyle Guide generated with LivingCSS
. -
globalStylesheets
- List of all CSS files to load in the<head>
of the style guide. -
menuButtonHTML
- HTML content of the menu button. Defaults toβ° Menu
. -
navbar
- List of page links for linking pages together in the<header>
navigation bar. Only set if there is more than one page defined. -
pageOrder
- List of page names in the order that they should be sorted. -
pages
- List of all pages and their sections. List will be sorted byoptions.sortOrder
. -
scripts
- List of all JavaScript files to load at the end of the style guide. -
sections
- List all root sections (sections without a parent) for the page and their children. List will be sorted byoptions.sortOrder
. -
stylesheets
- List of all CSS files to load in the examples of the style guide. If theoptions.loadcss
option is set, this list will contain all CSS files used to generate the style guide.NOTE: files will only be referenced by their file path and will not be copied over to the output directory. This ensures any relative file paths to images and other CSS files still work.
-
title
- Title of the style guide. Defaults to 'LivingCSS Style Guide'.
Using the context object in Handlebars
Let's say your comment looks like this:
/**
* A simple Button.
* @section Buttons
* @example <button>Click Me</button>
* @customTag Hello World
*/
This would generate a context object like so:
{
"sections": [{
"name": "Buttons",
"description": "<p>A simple Button.</p>",
"example": {
"description": "<button>Click me</button>",
"type": "markup"
},
"customTag": "Hello World"
}]
}
Then in handlebars you would do this:
Utility functions
LivingCSS has a few helpful utility functions that you can use in custom tags or in the options.preprocess
function.
-
livingcss.utils.getId(name)
- Get a hyphenated id from the name. Useful for generating ids for the DOM or a URL.livingcss.utils.getId('Section Name'); //=> 'section-name'
-
livingcss.utils.normalizeName(name)
- Normalize a name. Useful for comparisons ignoring case.livingcss.utils.normalizeName('Section Name'); //=> 'section name'
-
livingcss.utils.readFileGlobs(glob, callback)
- Pass a glob or array of globs to be read and a callback function that will be called for each read file. The function will be passed the file contents and the name of the file as parameters. Returns a Promise that is resolved when all files returned by the glob have been read. Useful for registering a glob of partials with Handlebars.var path = require('path'); livingcss('input.css', 'styleguide.html', { preprocess: function(context, template, Handlebars) { // register a glob of partials with Handlebars return livingcss.utils.readFileGlobs('partials/*.hb', function(data, file) { // make the name of the partial the name of the file var partialName = path.basename(file, path.extname(file)); Handlebars.registerPartial(partialName, data); }); } });
-
livingcss.utils.readFiles(files, callback)
- Pass a file or array of files to be read and a callback function that will be called for each read file. The function will be passed the file contents and the name of the file as parameters. Returns a Promise that is resolved when all files have been read.var path = require('path'); livingcss('input.css', 'styleguide.html', { preprocess: function(context, template, Handlebars) { // register a glob of partials with Handlebars return livingcss.utils.readFiles(['partials/one.hb', 'partials/two.hb'], function(data, file) { // make the name of the partial the name of the file var partialName = path.basename(file, path.extname(file)); Handlebars.registerPartial(partialName, data); }); } });