A lightweight Node.js theme engine.
Themeleon is two things:
- a conventional interface to build themes with Node.js,
- a tiny and modular framework to implement the interface.
A theme is a simple JavaScript function. The purpose of this function is to render a context in a destination.
The context is a JavaScript object. It typically contains the variables and configuration you will use to render the theme in the destination directory.
A theme is also asynchronous, and relies on Promises/A+ for this.
The following JSDoc comment describes the theme interface:
/**
* @param {String} destination
* Destination directory to render the theme in. The directory must
* already exist, be empty, and be writable.
*
* @param {Object} context
* An object passed to the template engine to render the theme. Usually,
* each key of the context object will be a variable in the template.
*
* @return {Promise}
* Any compatible implementation of the Promises/A+ specification.
*/
function render(destination, context) {}
So, if you want to create a Themeleon compatible theme, you can
implement this interface by hand without relying on the themeleon
Node.js package.
The behavior is undefined if the destination directory doesn't exists or is not empty.
Note: Themeleon does not document anything about the context object structure. This is left to the project that will be themed. It means that a Themeleon theme is strongly tied with the context it is designed for.
You will probably be publishing themes for a context, since you need a predictable idea of what will contain the context data.
The convention is to name the Node.js package like this:
{{context}}-theme-{{theme}}
The {{context}}
variable is the context name, an identifier that
qualifies the kind of data the context object will contain, and
{{theme}}
is the name of your theme.
See a practical example in the Examples section below.
You're not required to publish the theme on npm, anything that can be
require
d will do the job, so downloading a theme archive, extracting
it in a folder, and including its index.js
is also an acceptable
solution. I believe it's more practical to use a package manager for
this though.
If you want to make your project themable, all you'll need to do is to
dynamically require
a theme module, an call it with a destination and
context object.
Themeleon doesn't enforces anything about this, but the recommended way is the following:
- Let your users configure a theme name (or even directly a theme function if you expose a Node.js API).
- If the theme is not a function but a single identifier (something
like
[a-z-]+
, but you can be less strict),require
the prefixed name ({{context}}-theme-{{theme}}
). - Otherwise,
require
the whole name (it might be a path), resolved toprocess.cwd()
(it might be relative too).
Themeleon also acts as a theme creation framework, to ease the interface implementation by hiding relatively low-level considerations, like reading and writing files, handling promises, and using a raw template engine.
When using the framework, you only have to define a render function, that will describe the high-level tasks needed to render the theme.
These tasks are in fact mixins that you include in your Themeleon instance. It's easy to define and share custom mixins, and that's how template engines are brought to Themeleon.
The mixins makes it easy to abstract the theme directory, destination directory and the context data. For example, a mixin can decide to make a given path relative to the theme or destination directory when it makes sense, instead of the CWD, and a template engine mixin can implicitely pass the context object to the templates.
First, install Themeleon in your package.json
:
npm install themeleon --save
{
"dependencies": {
"themeleon": "1.*"
}
}
You will be using Themeleon in the index.js
of your theme package.
First, create a Themeleon instance:
var themeleon = require('themeleon')();
You can then use some mixins, for example see the supported template engines.
// Use consolidate.js for templating
themeleon.use('consolidate');
You may also add your own mixin:
themeleon.use({
/**
* Log current source directory, destination directory, and context
* variables.
*/
log: function () {
console.log(this.src, this.dest, this.ctx);
},
});
Now everything is initialized, you can begin to describe your theme.
For this, you define a function taking a Themeleon instance t
as
parameter, and you give this function to the themeleon
function,
together with the theme directory (certainly __dirname
if you're in
the index.js
):
module.exports = themeleon(__dirname, function (t) {
// Theme render logic here
});
The themeleon
function will take care of the
interface implementation, and will call your render
function on demand. That's where the magic resides:
module.exports = themeleon(__dirname, function (t) {
t.copy('assets'); // Will copy `assets` in destination directory
// t.copy('assets', 'foo'); // Other name in destination directory
// Compile a Swig view as `index.html` in destination directory
t.swig('views/index.html.swig', 'index.html');
// Call the custom mixin defined above
t.log();
});
And that's all! Themeleon will run all the tasks and return a promise waiting for them all to complete.
Use the above theme (assuming it's named project-theme-foo
) in a
project:
var theme = require('project-theme-foo');
// Render the theme in `dest` directory with given variables
theme('dest', {
some: 'variables',
that: 'will be',
passed: 2,
the: 'theme',
});
Themeleon provides a native mixin for consolidate.js, an awesome library to get an unified interface for a bunch of template engines.
Basically, t.{{engine}}
is an adapter for consolidate.{{engine}}
,
taking care of passing the context and writing the destination file for
you. All you have to do is run t.{{engine}}(src, dest)
.
If you want to add more variables to the context object (passed to the templates) for a specific file, you can pass an object as third argument, which will be merged with the global context before rendering. This is also used by consolidate.js to define partials for some template engines.
The best production example for Themeleon is SassDoc. In fact, Themeleon was created for SassDoc because we wanted to support custom themes, without having any theming logic inside SassDoc.
SassDoc describes a theme context interface. This documents the context object passed to SassDoc themes. So, if you want to write a theme for SassDoc, all you have to do is to provide a function implementing the Themeleon interface, and making sense of the context data passed by SassDoc.
Since the context is specific to SassDoc, all themes are prefixed with
sassdoc-theme-
. Thus, the default theme is
published as sassdoc-theme-default
. This theme uses Swig to render a
single HTML page from multiple Swig partials, built around the
documented SassDoc context.