Comparing version 0.9.3 to 1.0.0
#!/usr/bin/env node | ||
var blake = require('../index') | ||
, cop = require('cop') | ||
, files = require('../lib/read').fstream | ||
, copy = require('../lib/copy') | ||
, join = require('path').join | ||
var cop = require('cop') | ||
var fs = require('fs') | ||
var path = require('path') | ||
;(function () { | ||
var arg = process.argv.splice(2) | ||
, isValid = arg && arg.length >= 2 | ||
if (!isValid) { | ||
return console.error('Usage: blake source_directory target_directory [source_file ...]'); | ||
if (!arg.length) { | ||
return console.error('Usage: blake [source_directory] target_directory [source_file ...]') | ||
} | ||
var cwd = process.cwd() | ||
function isFile () { | ||
var p = path.resolve(cwd, arg[1]) | ||
return fs.statSync(p).isFile() | ||
} | ||
var sansSource = arg.length === 1 || isFile() | ||
if (sansSource) { | ||
arg.unshift(cwd) | ||
} | ||
var source = arg.shift() | ||
, target = arg.shift() | ||
var target = arg.shift() | ||
function onerror (er) { | ||
console.error(er.message) | ||
} | ||
var b = blake(source, target) | ||
if (!arg.length) { | ||
copy(join(source, 'resources'), target) | ||
.on('error', console.error) | ||
blake.copy(b.resources, target) | ||
.on('error', onerror) | ||
.on('end', bake) | ||
@@ -28,15 +36,10 @@ .pipe(process.stdout) | ||
} | ||
function bake () { | ||
files(source, arg) | ||
.pipe(cop('path')) | ||
.pipe(blake(source, target)) | ||
.pipe(cop(format)) | ||
var p = arg instanceof Array && arg.length ? arg : b.data | ||
blake.files(p) | ||
.pipe(b) | ||
.on('error', onerror) | ||
.pipe(cop(function (str) { return str + '\n' })) | ||
.pipe(process.stdout) | ||
} | ||
})() | ||
function format (str) { | ||
return str += '\n' | ||
} | ||
319
index.js
@@ -1,48 +0,301 @@ | ||
// blake - generate site | ||
var Transform = require('stream').Transform | ||
, fs = require('fs') | ||
, mkdirp = require('mkdirp') | ||
, dirname = require('path').dirname | ||
, writeFile = require('fs').writeFile | ||
, StringDecoder = require('string_decoder').StringDecoder | ||
, read = require('./lib/read.js').readItems | ||
, conf = require('./lib/read.js').conf | ||
module.exports = Blake | ||
module.exports = function (s, target) { | ||
var c = s && target ? conf(s, target) : s | ||
, decoder = new StringDecoder('utf8') | ||
, stream = new Transform({ objectMode:true }) | ||
module.exports.copy = copy | ||
module.exports.files = files | ||
stream._transform = function (chunk, encoding, cb) { | ||
var filename = decoder.write(chunk) | ||
read(c)(filename, function (er, item) { | ||
bake(item, function (er) { | ||
stream.push(item.path) | ||
cb(er) | ||
var LRU = require('lru-cache') | ||
var createHash = require('crypto').createHash | ||
var cop = require('cop') | ||
var fs = require('fs') | ||
var fstream = require('fstream') | ||
var mkdirp = require('mkdirp') | ||
var path = require('path') | ||
var readArray = require('event-stream').readArray | ||
var stream = require('stream') | ||
var string_decoder = require('string_decoder') | ||
var util = require('util') | ||
function Blake (source, target) { | ||
if (!(this instanceof Blake)) return new Blake(source, target) | ||
stream.Transform.call(this, { objectMode: true }) | ||
this._conf = conf(source, target) | ||
this._reader = new ItemReader(this._conf) | ||
this._decoder = new string_decoder.StringDecoder() | ||
var me = this | ||
Object.defineProperty(this, 'resources', { | ||
get: function () { return me._conf.paths.resources } | ||
}) | ||
Object.defineProperty(this, 'data', { | ||
get: function () { return me._conf.paths.data } | ||
}) | ||
} | ||
util.inherits(Blake, stream.Transform) | ||
function write (p, data, cb) { | ||
mkdirp(path.dirname(p), function (er, made) { | ||
er ? cb(er) : fs.writeFile(p, data, cb) | ||
}) | ||
} | ||
Blake.prototype._transform = function (chunk, enc, cb) { | ||
var me = this | ||
var filename = this._decoder.write(chunk) | ||
function view (item, cb) { | ||
if (!item.view) { | ||
cb(new Error('blake: ' + item.name + ' misses view function')) | ||
return | ||
} | ||
item.read = function (p, cb) { | ||
me.items(p, cb) | ||
} | ||
item.view(item, function (error, result) { | ||
if (error) { | ||
var msg = ['blake', 'view error', item.path, error.message].join(': ') | ||
var er = new Error(msg) | ||
er.item = item | ||
return me.emit('error', er) | ||
} | ||
write(item.path, result, function (error) { | ||
if (error) { | ||
cb(new Error('blake: write error: ' + error.message)) | ||
} else { | ||
cb() | ||
} | ||
}) | ||
}) | ||
} | ||
function bake (item, cb) { | ||
if (!item.bake) { | ||
cb(new Error('Undefined bake function for ' + item.name)) | ||
return | ||
me._reader.read(filename, function (er, item) { | ||
if (er) return cb(er) | ||
view(item, function (er) { | ||
me.push(item.path) | ||
cb(er) | ||
}) | ||
}) | ||
} | ||
Blake.prototype.items = function (p, cb) { | ||
this._reader.read(p, cb) | ||
} | ||
function item (conf, file, str) { | ||
var marker = '\n\n' | ||
var tokens = str.split(marker) | ||
var paths = conf.paths | ||
var h = header(tokens.shift(), file, paths) | ||
var body = tokens.join(marker) | ||
var views = conf.views | ||
return new Item( | ||
h, | ||
body, | ||
paths, | ||
h.title, | ||
h.name, | ||
h.date, | ||
path.join(paths.templates, h.template), | ||
path.join(paths.target, h.path, h.name), | ||
path.join(h.path, h.name), | ||
views[h.template] // view | ||
) | ||
} | ||
function header (data, file, paths) { | ||
var h = null | ||
try { | ||
h = JSON.parse(data) | ||
} catch (error) { | ||
throw new Error('blake: header error: ' + file + ': ' + error.message) | ||
} | ||
if (!h.template) { | ||
throw new Error('blake: template required: ' + file) | ||
} | ||
if (!h.name) { | ||
var name = path.basename(file).split('.')[0] | ||
var extension = name === 'atom' || name === 'rss' ? 'xml' : 'html' | ||
h.name = [name, '.', extension].join('') | ||
} | ||
h.title = h.title || null | ||
h.date = h.date ? new Date(h.date) : new Date() | ||
h.path = h.path || path.dirname(file).split(paths.posts)[1] || '' | ||
return h | ||
} | ||
function Item ( | ||
header | ||
, body | ||
, paths | ||
, title | ||
, name | ||
, date | ||
, templatePath | ||
, path | ||
, link | ||
, view) { | ||
this.header = header | ||
this.body = body | ||
this.paths = paths | ||
this.title = title | ||
this.name = name | ||
this.date = date | ||
this.templatePath = templatePath | ||
this.path = path | ||
this.link = link | ||
this.view = view | ||
} | ||
function files (p) { | ||
if (p instanceof Array && p.length) { | ||
return readArray(p) | ||
} else { | ||
var filter = cop(function (entry) { return entry.path }) | ||
var reader = new fstream.Reader({ path: p }) | ||
return reader.pipe(filter) | ||
} | ||
} | ||
function Paths (resources, data, templates, posts, target) { | ||
this.resources = resources | ||
this.data = data | ||
this.templates = templates | ||
this.posts = posts | ||
this.target = target | ||
} | ||
function paths (source, target, conf) { | ||
var p = conf.paths | ||
var resources = p.resources ? path.join(source, p.resources) : null | ||
var data = p.data ? path.join(source, p.data) : null | ||
var templates = p.templates ? path.join(source, p.templates) : null | ||
var posts = p.posts ? path.join(source, p.posts) : null | ||
return new Paths(resources, data, templates, posts, target) | ||
} | ||
function conf (source, target) { | ||
if (typeof source === 'string') { | ||
if (typeof target !== 'string') { | ||
target = source | ||
source = process.cwd() | ||
} | ||
item.read = read(c) | ||
item.bake(item, function (er, result) { | ||
if (er) { | ||
stream.emit('error', er) | ||
return | ||
} | ||
var uc = require(source) | ||
var res = Object.create(null) | ||
res.paths = paths(source, target, uc) | ||
res.views = uc.views | ||
return res | ||
} | ||
function ItemReader (conf) { | ||
if (!(this instanceof ItemReader)) return new ItemReader(conf) | ||
this.conf = conf | ||
this.decoder = new string_decoder.StringDecoder() | ||
this.cache = new LRU({ max: 500, maxAge: 1000 * 60 * 3 }) | ||
} | ||
ItemReader.prototype.read = function (p, cb) { | ||
if (!p) { | ||
cb(new Error('blake: read error: no path')) | ||
return | ||
} | ||
var me = this | ||
fs.stat(p, function (er, stats) { | ||
if (stats.isDirectory()) { | ||
me.readDirectory(p, cb) | ||
} else { | ||
me.readFile(p, cb) | ||
} | ||
}) | ||
} | ||
function hash (str) { | ||
return createHash('md5').update(str).digest('hex') | ||
} | ||
ItemReader.prototype.readFile = function (filename, cb) { | ||
var k = hash(filename) | ||
if (this.cache.has(k)) { | ||
return cb(null, this.cache.get(k)) | ||
} | ||
var me = this | ||
var cache = this.cache | ||
fs.readFile(filename, function (er, data) { | ||
var it = item(me.conf, filename, me.decoder.write(data)) | ||
Object.defineProperty(it, 'template', { | ||
get: function () { | ||
var k = hash(it.templatePath) | ||
if (cache.has(k)) return cache.get(k) | ||
var buf = fs.readFileSync(it.templatePath) | ||
cache.set(k, buf) | ||
return buf | ||
} | ||
write(item.path, result, cb) | ||
}) | ||
cache.set(k, it) | ||
return cb(er, it) | ||
}) | ||
} | ||
ItemReader.prototype.readDirectory = function (p, cb) { | ||
var reader = fstream.Reader({ path: p }) | ||
var s = new stream.Writable() | ||
var items = [] | ||
var me = this | ||
s.add = function (entry) { | ||
if (entry.type === 'Directory') { | ||
entry.on('entry', s.add) | ||
return true | ||
} | ||
me.readFile(entry.path, function (er, item) { | ||
items.push(item) | ||
entry.depth === 1 ? s.end() : reader.resume() | ||
}) | ||
return false | ||
} | ||
return stream | ||
s.end = function () { | ||
cb(null, items) | ||
} | ||
reader.pipe(s) | ||
} | ||
function write (path, data, callback) { | ||
mkdirp(dirname(path), function (err, made) { | ||
writeFile(path, data, callback) | ||
function filter (entry) { | ||
return !entry.basename.match(/^\./) | ||
} | ||
function copy (source, target, cb) { | ||
var reader = fstream.Reader({ path: source, filter: filter }) | ||
var writer = fstream.Writer({ path: target, type: 'Directory' }) | ||
var s = new stream.PassThrough() | ||
function push (entry) { | ||
if (entry.type === 'File') { | ||
var p = path.join(target, entry.path.split(source)[1] || '') | ||
s.push(p + '\n') | ||
} else { | ||
entry.on('entry', push) | ||
} | ||
} | ||
reader.on('error', function (err) { | ||
s.emit('error', err) | ||
}) | ||
reader.on('entry', function (entry) { | ||
push(entry) | ||
}) | ||
writer.on('error', function (err) { | ||
s.emit('error', err) | ||
}) | ||
writer.on('end', function () { | ||
s.push(null) | ||
if (cb) cb() | ||
}) | ||
reader.pipe(writer) | ||
return s | ||
} | ||
if (parseInt(process.env.NODE_TEST, 10) === 1) { | ||
module.exports.ItemReader = ItemReader | ||
module.exports.conf = conf | ||
module.exports.header = header | ||
module.exports.item = item | ||
module.exports.paths = paths | ||
module.exports.write = write | ||
} |
{ | ||
"name": "blake", | ||
"version": "0.9.3", | ||
"description": "Simple, blog aware infrastructure to generate static sites", | ||
"version": "1.0.0", | ||
"description": "Generate static sites", | ||
"main": "index.js", | ||
"directories": { | ||
"lib": "lib", | ||
"example": "example", | ||
"test": "test" | ||
}, | ||
"scripts": { | ||
"test": "tap test/*.js" | ||
"test": "NODE_TEST=1 tap test/*.js", | ||
"posttest": "rm -rf /tmp/blake-[1-9]*" | ||
}, | ||
@@ -26,3 +25,3 @@ "repository": { | ||
"generator", | ||
"infrastructure" | ||
"build" | ||
], | ||
@@ -34,17 +33,15 @@ "bin": { | ||
"cop": "^0.3.6", | ||
"event-stream": "^3.1.7", | ||
"fstream": "^0.1.31", | ||
"lru-cache": "^2.5.0", | ||
"mkdirp": "^0.4.2", | ||
"popfun": "^0.1.2", | ||
"event-stream": "^3.3.1", | ||
"fstream": "^1.0.7", | ||
"lru-cache": "^2.6.5", | ||
"mkdirp": "^0.5.1", | ||
"popfun": "^1.0.0", | ||
"prettydate": "0.0.1" | ||
}, | ||
"devDependencies": { | ||
"jade": "^1.8.1", | ||
"markdown": "^0.5.0", | ||
"rimraf": "^2.2.8", | ||
"tap": "^0.4.13" | ||
"rimraf": "^2.4.3", | ||
"tap": "^1.4.0" | ||
}, | ||
"engines": { | ||
"node": "0.10.x" | ||
"node": ">0.10" | ||
}, | ||
@@ -51,0 +48,0 @@ "author": { |
355
README.md
@@ -1,253 +0,214 @@ | ||
# blake - generate site | ||
# blake - generate anything | ||
The `blake` [Node.js](http://nodejs.org/) module provides a simple, blog aware infrastructure to generate [static sites](http://troubled.pro/2012/05/static-websites.html). For unrestricted choice of input formats and template languages, `blake` confines itself to IO and template routing; it delegates artifact generation to user-written functions. | ||
The **blake** [Node](https://nodejs.org/) package provides a file generation pipeline. I wrote it to generate my [site](http://troubled.pro/). Separating IO from data transformation—by using an intermediate representation—**blake** takes care of IO, and lets you get on with generating your stuff. Of course, [gulp](http://gulpjs.com/) puts itself forward as a streaming build system, but if you—like me—experience slight framework fatigue, and prefer plain Node, you might want to give **blake** a shot. | ||
[![Build Status](https://travis-ci.org/michaelnisi/blake.png)](http://travis-ci.org/michaelnisi/blake) [![David DM](https://david-dm.org/michaelnisi/blake.png)](http://david-dm.org/michaelnisi/blake) | ||
[![Build Status](https://travis-ci.org/michaelnisi/blake.png)](http://travis-ci.org/michaelnisi/blake) | ||
## CLI Usage | ||
## Example | ||
Alas, I don't have a silly example yet, however, you can generate my site to get an understanding of how to use **blake**: | ||
``` | ||
blake source_directory target_directory | ||
blake source_directory target_directory source_file ... | ||
git clone https://github.com/michaelnisi/troubled.git | ||
cd troubled | ||
npm install | ||
blake /tmp/troubled | ||
``` | ||
In the first form, `blake` writes all files generated from input data in the `source_directory` to the `target_directory`. In the second synopsis form, output is generated from the specified source files only. | ||
## Library Usage | ||
Start an HTTP server in the target directory: | ||
### Generate from directory | ||
```js | ||
var blake = require('blake') | ||
, source = 'blake-site' | ||
, target = '/tmp/blake-site' | ||
, join = require('path').join | ||
, Reader = require('fstream').Reader | ||
, props = { path:join(source, 'data') } | ||
, cop = require('cop') | ||
new Reader(props) | ||
.pipe(cop('path')) | ||
.pipe(blake(source, target)) | ||
.pipe(cop(function (filename) { return filename + '\n' })) | ||
.pipe(process.stdout) | ||
``` | ||
### Copy static resources and generate from directory | ||
```js | ||
var blake = require('blake') | ||
, join = require('path').join | ||
, source = join(process.cwd(), './blake-site') | ||
, target = '/tmp/blake-site' | ||
, Reader = require('fstream').Reader | ||
, props = { path:join(source, 'data') } | ||
, cop = require('cop') | ||
, copy = require('../lib/copy.js') | ||
copy(join(source, 'resources'), target) | ||
.on('error', console.error) | ||
.on('end', function () { | ||
new Reader(props) | ||
.pipe(cop('path')) | ||
.pipe(blake(source, target)) | ||
.pipe(cop(function (filename) { return filename + '\n' })) | ||
.pipe(process.stdout) | ||
}) | ||
.pipe(process.stdout) | ||
cd /tmp/troubled | ||
python -m SimpleHTTPServer 8081 | ||
``` | ||
### Generate from files | ||
```js | ||
var blake = require('blake') | ||
, cop = require('cop') | ||
, readArray = require('event-stream').readArray | ||
, filenames = ['first/file', 'second/file', 'third/file'] | ||
, source = 'source_directory' | ||
, target = 'target_directory' | ||
readArray(filenames) | ||
.pipe(blake(source, target)) | ||
.pipe(cop(function (filename) { return filename + '\n' })) | ||
.pipe(process.stdout) | ||
Point your browser to `http://localhost:8081` to inspect the result. | ||
## CLI | ||
``` | ||
### Generate and push to S3 | ||
blake [source_directory] target_directory [source_file ...] | ||
``` | ||
Since the `blake` function returns a [Transform](http://nodejs.org/api/stream.html#stream_class_stream_transform) stream that emits the paths of the generated artifacts, we can pipe to [pushup](https://github.com/michaelnisi/pushup), and upload the files directly to S3. | ||
```js | ||
var resolve = require('path').resolve | ||
, cop = require('cop') | ||
, getProps = require('pushup/lib/getProps') | ||
, blake = require('blake') | ||
, pushup = require('pushup') | ||
, Reader = require('fstream').Reader | ||
, sep = require('path').sep | ||
, source = 'source_directory' | ||
, target = '/tmp/target_directory' | ||
, reader = new Reader({ path:resolve(source, 'data') }) | ||
, props = getProps() | ||
If you skip passing the source directory, and give the target only, like in the example, **blake** will use the current working directory as source. | ||
process.chdir(target) | ||
reader | ||
.pipe(cop('path')) | ||
.pipe(blake(source, target)) | ||
.pipe(cop(adjustPath)) | ||
.pipe(pushup(props)) | ||
.pipe(process.stdout) | ||
To generate specific files exclusively, you can pass arbitary source file paths. | ||
function adjustPath (p) { | ||
return p.split(sep).slice(3).join(sep) | ||
} | ||
``` | ||
## Types | ||
### str() | ||
Optional String type, which can be `String()`, `null`, or `undefined`. | ||
### err() | ||
Optional error type: `Error()`, `null`, or `undefined`. | ||
### paths() | ||
An object to configure paths. | ||
- `resources` `str()` An optional path for static resources. | ||
- `data` `String()` The path to the content directory. | ||
- `templates` `String()` The folder to load templates from. | ||
- `posts` `str()` An optional posts directory for bloglike sites. | ||
- `target` `String()` The target directory. | ||
### header() | ||
Just a bag of things, passed to your **view function**, you can put anything you want in here; just remember that **blake** uses these properties internally: | ||
- `template` `String()` The filename of the template. | ||
- `title` `str()` The title of the item or `null`. | ||
- `date` `str()` This optional date overrides the current date. | ||
- `path` `str()` If this optional target path is not provided, **blake** will mirror the path of the source file. | ||
- `name` `str()` A target filename to override the default source filename. | ||
### item() | ||
The item object, an intermediate representation of each file to generate, forms the core of **blake**; it is constructed internally and passed to the **view function**, in which you use its properties to produce the final output. | ||
- `header` `header()` The original header. | ||
- `paths` `paths()` The paths object populated by **blake**. | ||
- `body` `str()` Everything after the header in the source file. | ||
- `title` `str()` The title of the item. | ||
- `name` `String()` The target file name. | ||
- `date` `Date()` The current date or the date set in `header()`. | ||
- `templatePath` `String()` The absolute path to the template file. | ||
- `path` The absolute target file path. | ||
- `link` The relative target file path applicable as local link on the site. | ||
- `template` `Buffer()` The template data. | ||
- `read` `read()` | ||
`read(path, cb)` | ||
Reads all source files in the given path recursively to apply the callback with an optional error and the resulting items—handy to create archives, feeds, etc. | ||
- `path` `String()` | ||
- `cb` `Function(er, items)` | ||
- `er` `err()` An optional Error. | ||
- `items` `[item()]` The resulting items. | ||
### views() | ||
An object that maps **view functions** by template filename. | ||
## API | ||
### blake(source, target) | ||
The **blake** module exports the constructor of the `Blake` Transform stream. To use it do `require('blake')`. A `Blake` stream has two additional getters providing access to parts of the configuration: | ||
The `blake` module exports a single function that returns a [Transform](http://nodejs.org/api/stream.html#stream_class_stream_transform) stream. While writing source filenames to it, you can read target filenames (of written arfifacts) from it. | ||
- `resources` `str()` The optional path to the resources directory. | ||
- `data` `String()` The path to the data source directory. | ||
- `source` The source directory. | ||
- `target` The target directory. | ||
This constructor—the solely exported function by the module—is decorated with two stateless functions, given `var blake = require('blake')`: | ||
## Configuration | ||
`blake.files(path)` | ||
`blake` requires a configuration module at `source_directory/config.js`, which exports `paths`, and `views` (a map of generator functions): | ||
Returns a readable stream of all filenames in the given directory. The filenames are read recursively, directory names are skipped. | ||
- `path` `String()` The path of the directory to read. | ||
`blake.copy(source, target)` | ||
Recursively copies all files from the `source` directory to the `target` directory; returns a readable stream of the copied target filenames. | ||
- `source` `String()` The source directory. | ||
- `target` `String()` The target directory. | ||
### Configuring a site | ||
The source directory has to contain a source module, which has to export `paths()` and `views()`: | ||
```js | ||
exports.paths = { | ||
data: 'data' | ||
, templates: 'templates' | ||
, resources: 'resources' | ||
, posts: 'data/posts' | ||
data: 'data', | ||
templates: 'templates', | ||
resources: 'resources', | ||
posts: 'data/posts' | ||
} | ||
// Associate your view functions with template names. | ||
exports.views = { | ||
'rss.jade': require('./rss.js') | ||
, 'article.jade': require('./article.js') | ||
, 'home.jade': require('./home.js') | ||
, 'about.jade': require('./about.js') | ||
, 'archive.jade': require('./archive.js') | ||
'rss.jade': rss, | ||
'article.jade': article, | ||
'home.jade': home, | ||
'about.jade': about, | ||
'error.jade': about, | ||
'archive.jade': archive, | ||
'likes.jade': likes, | ||
'tweet.jade': tweet | ||
} | ||
``` | ||
The `paths` object defines input paths, with two required directories: `data` and `templates`. From `data` blake loads general input data, `templates` is the directory for templates. The two optional directories are `resources` and `posts`. The content of `resources` is copied to the `target_directory' unchanged. The `posts` directory hosts blog posts. | ||
The `views` object is a map of user-written functions that implement the actual generation of output artifacts. Here, these functions are mapped by template name. | ||
### Writing a view | ||
## Input | ||
`view (item, cb)` | ||
At the top of each input file blake expects a JSON string that is interpreted as header providing transformation parameters. Besides it can contain additional user defined data—the `item` parameter, passed to the view functions, provides a reference to the raw header. Input data for a blog entry could look like so: | ||
``` | ||
{ | ||
"title": "Example", | ||
"description": "An example article", | ||
"template": "article.jade", | ||
"date": "2012-03-21" | ||
} | ||
- `item` `item()` | ||
- `cb` `Function(error, result)` | ||
- `error` `err()` Pass an error if something went wrong. | ||
- `result` `Buffer()` The resulting artifact generated by this view. | ||
Your highness, when I said that you are like a stream of bat's piss, | ||
I only mean that you shine out like a shaft of gold when all around | ||
it is dark. | ||
``` | ||
The end of the header is marked by an empty line. Everything that follows is interpreted as content and is passed to the views untouched. | ||
Each `item()` is associated with a **view function** by a template name in the configuration. This function—implemented by you—is responsible to generate the artifact. It is in this function where the actual work is done. Here you use values from the item and your template, also provided via the item, to generate the final output, which you apply to the callback once you are done—so it can be written to disk by **blake**. | ||
### Header | ||
### Creating a new instance | ||
JSON at the top of an input file: | ||
```js | ||
{ | ||
"title": "Example", | ||
"description": "An example article", | ||
"template": "article.jade", | ||
"date": "2012-03-21", | ||
"path": "2012/03", | ||
"name": "example" | ||
} | ||
``` | ||
* `title` is the title of the page (optional) | ||
* `description` is the description of the page or rather the post (optional) | ||
* `template`is the filename of the template to use (required) | ||
* `date` is the publish date, if not provided it's set to `NOW` (optional) | ||
* `path` is the output path, if not provided the path of the input file is used (optional) | ||
* `name` is used as filename of the output file, if not provided the filename of the input file is used (optional) | ||
`blake(source, target)` | ||
The source object, passed to the views, provides a reference to the raw header object. Thus, the header is extendable with arbritrary fields, which can be interpreted by the generators (written by you). | ||
- `source` `str()` The source directory. | ||
- `target` `String()` The target directory. | ||
If you decide to mirror the input paths in your output, you can omit path and name. In that case a typical header of a blog post might look like the following: | ||
```js | ||
{ | ||
"title": "Example", | ||
"description": "An example article", | ||
"template": "article.jade", | ||
"date": "2012-03-21", | ||
} | ||
var blake = require('blake') | ||
var b = blake('./test/data', '/tmp/blake-example') | ||
``` | ||
Input data with this header, located at `source_directory/data/posts/2012/03/example.md`, would produce `2012/03/article.html`. | ||
An input file can consist of just a header (without content) to generate, for example, an RSS feed. | ||
```js | ||
{ | ||
"title": "Blog", | ||
"description": "Stuff I write", | ||
"link": "http://my.blog", | ||
"template": "rss.jade", | ||
"name": "rss.xml" | ||
} | ||
### Copying static resources | ||
`blake.copy(resources, target)` | ||
- `resources` `String()` A directory containing static resources. | ||
- `target` `String()` The target directory. | ||
``` js | ||
var b = blake(source, target) | ||
blake.copy(b.resources, target) | ||
``` | ||
### Views | ||
The views—alternative naming would be: transformers, generators, or bakers—are the functions that generate your artifacts; they have the following signature: | ||
### Generating a site | ||
For a complete build, you would typically generate the site after copying static resources: | ||
```js | ||
function (item, callback) | ||
var b = blake(source, target) | ||
blake.copy(b.resources, target).on('end', function () { | ||
blake.files(b.data).pipe(b) | ||
}).resume() | ||
``` | ||
The passed in 'item' provides the input data to generate the artifact (or most likely the page). | ||
Here, for example, an `item` representing a blog post: | ||
```js | ||
{ header: | ||
{ title: 'Static Websites', | ||
description: '...', | ||
template: 'article.jade', | ||
data: Thu May 17 2012 02:00:00 GMT +0200 (CEST), | ||
path: '2012/05', | ||
name: 'static-websites.html' } | ||
body: '...', | ||
paths: | ||
{ target: '/tmp/michaelnisi-site', | ||
resources: '/Users/michael/workspace/michaelnisi/resources', | ||
data: '/Users/michael/workspace/michaelnisi/data', | ||
templates: '/Users/michael/workspace/michaelnisi/templates', | ||
posts: '/Users/michael/workspace/michaelnisi/data/posts' }, | ||
title: 'Static Websites', | ||
name: 'static-websites.html', | ||
date: Thu May 17 2012 02:00:00 GMT+0200 (CEST), | ||
templatePath: '/Users/michael/workspace/michaelnisi/templates/article.jade', | ||
path: '/tmp/michaelnisi-site/2012/05/static-websites.html', | ||
link: '2012/05/static-websites', | ||
dateString: 'Thu May 17 2012', | ||
bake: [Function], | ||
template: <Buffer 0a 20 20 20 20 64 69 76 ...> } | ||
### Generating specific files | ||
Since **blake** is a Transform stream, you can easily generate only specific files: | ||
``` | ||
To see a simple example: | ||
var b = blake(source, target) | ||
b.end('path/to/a/file') | ||
``` | ||
git clone git://github.com/michaelnisi/blake.git | ||
cd blake/example | ||
npm install | ||
node generate.js | ||
open /tmp/blake-site/index.html | ||
``` | ||
To evaluate a more elaborate example, you might generate my [blog](http://troubled.pro), for which I use [Jade](http://jade-lang.com/) and [Markdown](http://daringfireball.net/projects/markdown/): | ||
``` | ||
npm install -g blake | ||
git clone git://github.com/michaelnisi/troubled.git | ||
cd troubled | ||
npm install | ||
blake . /tmp/troubled-site | ||
``` | ||
## Deployment | ||
Of course you can build your site locally, and upload it to your webserver manually; but I recommend to run blake on a server, using [post-receive hooks](http://help.github.com/post-receive-hooks/) to automatically generate your site, post to each push your input data repository receives. | ||
## Install | ||
## Installation | ||
With [npm](https://npmjs.org/package/blake) do: | ||
[![npm](https://nodei.co/npm/blake.png?compact=true)](https://npmjs.org/package/blake) | ||
``` | ||
npm install blake | ||
``` | ||
To use the command-line interface: | ||
``` | ||
npm install -g blake | ||
``` | ||
## License | ||
[MIT License](https://raw.github.com/michaelnisi/blake/master/LICENSE) |
@@ -0,30 +1,36 @@ | ||
var cop = require('cop') | ||
var copy = require('../').copy | ||
var es = require('event-stream') | ||
var fs = require('fs') | ||
var fstream = require('fstream') | ||
var rimraf = require('rimraf') | ||
var path = require('path') | ||
var test = require('tap').test | ||
, copy = require('../lib/copy.js') | ||
, join = require('path').join | ||
, fs = require('fs') | ||
, rimraf = require('rimraf') | ||
, source = '../example/blake-site/resources' | ||
, target = '/tmp/blake-' + Math.floor(Math.random() * (1<<24)) | ||
, fstream = require('fstream') | ||
, es = require('event-stream') | ||
, cop = require('cop') | ||
var common = require('./lib/common') | ||
var source = common.source(__dirname, 'resources') | ||
var target = common.freshTarget() | ||
function paths (p, cb) { | ||
return fstream.Reader({ path: p }) | ||
.pipe(cop('path')) | ||
.pipe(es.writeArray(cb)) | ||
} | ||
test('directory', function (t) { | ||
copy(source, target, function (err) { | ||
var paths = [ | ||
join(target, 'css', 'style.css') | ||
, join(target, 'img', 'bg.png') | ||
copy(source, target, function (er) { | ||
if (er) throw er | ||
t.ok(fs.statSync(target).isDirectory(), 'should exist') | ||
var wanted = [ | ||
path.join(target, 'bye'), | ||
path.join(target, 'hello') | ||
] | ||
t.ok(fs.statSync(target).isDirectory(), 'should exist') | ||
paths.forEach(function (p) { | ||
wanted.forEach(function (p) { | ||
t.ok(fs.statSync(p), 'should exist') | ||
}) | ||
fstream.Reader({ path:target }) | ||
.pipe(cop('path')) | ||
.pipe(es.writeArray(function (err, lines) { | ||
t.deepEqual(lines, paths, 'should equal paths') | ||
t.end() | ||
})) | ||
paths(target, function (er, found) { | ||
if (er) throw er | ||
t.deepEqual(found, wanted, 'should have copied all files') | ||
t.end() | ||
}) | ||
}) | ||
@@ -34,8 +40,6 @@ }) | ||
test('teardown', function (t) { | ||
rimraf(target, function (err) { | ||
fs.stat(target, function (err) { | ||
t.ok(!!err, 'should error') | ||
t.end() | ||
}) | ||
rimraf(target, function (er) { | ||
if (er) throw er | ||
t.end() | ||
}) | ||
}) |
@@ -1,25 +0,31 @@ | ||
var blake = require('../index.js') | ||
, es = require('event-stream') | ||
, path = require('path') | ||
, cop = require('cop') | ||
, fstream = require('fstream') | ||
, test = require('tap').test | ||
, config = require('./config.js') | ||
, source = config.source | ||
, target = config.target | ||
var blake = require('../') | ||
var common = require('./lib/common') | ||
var cop = require('cop') | ||
var es = require('event-stream') | ||
var fstream = require('fstream') | ||
var path = require('path') | ||
var test = require('tap').test | ||
var wanted = [ | ||
path.join(target, 'index.html') | ||
] | ||
var source = common.source(__dirname) | ||
var target = common.freshTarget() | ||
test('files', function (t) { | ||
var p = path.resolve(source, 'data') | ||
var s = blake.files(p) | ||
t.plan(1) | ||
s.on('data', function (chunk) { | ||
var wanted = path.resolve(p, 'index.md') | ||
t.is(chunk, wanted) | ||
}) | ||
}) | ||
test('read array of filenames', function (t) { | ||
var filenames = [ | ||
path.join(source, 'data', 'index.md') | ||
] | ||
var filenames = [path.join(source, 'data', 'index.md')] | ||
es.readArray(filenames) | ||
.pipe(blake(source, target)) | ||
.pipe(es.writeArray(function (err, array) { | ||
t.equal(array.length, wanted.length) | ||
t.deepEqual(array, wanted) | ||
.pipe(es.writeArray(function (er, names) { | ||
if (er) throw er | ||
var wanted = [path.join(target, 'index.html')] | ||
t.is(names.length, wanted.length) | ||
t.deepEqual(names, wanted) | ||
t.end() | ||
@@ -30,9 +36,10 @@ })) | ||
test('files written', function (t) { | ||
fstream.Reader({ path:target }) | ||
fstream.Reader({ path: target }) | ||
.pipe(cop('path')) | ||
.pipe(es.writeArray(function (err, lines) { | ||
.pipe(es.writeArray(function (er, lines) { | ||
if (er) throw er | ||
var wanted = [path.join(target, 'index.html')] | ||
t.deepEqual(lines, wanted, 'should equal paths') | ||
t.end() | ||
})) | ||
t.end() | ||
}) |
@@ -0,24 +1,26 @@ | ||
var blake = require('../') | ||
var common = require('./lib/common') | ||
var path = require('path') | ||
var readFileSync = require('fs').readFileSync | ||
var string_decoder = require('string_decoder') | ||
var test = require('tap').test | ||
, item = require('../lib/item').item | ||
, header = require('../lib/item').header | ||
, strftime = require('prettydate').strftime | ||
, path = require('path') | ||
, readFileSync = require('fs').readFileSync | ||
, config = require('./config.js') | ||
, target = config.target | ||
, props = config.props | ||
, paths = props.paths | ||
var source = common.source(__dirname) | ||
var target = common.freshTarget() | ||
var config = blake.conf(source, target) | ||
var paths = config.paths | ||
test('header', function (t) { | ||
var f = blake.header | ||
var file = path.join(paths.data, 'index.md') | ||
t.throws(function () { | ||
header(null, file, paths) | ||
f(null, file, paths) | ||
}) | ||
t.throws(function () { | ||
var data = '{"title":"Title"}' | ||
header(data, file, paths) | ||
f(data, file, paths) | ||
}) | ||
t.doesNotThrow(function () { | ||
var data = '{"template":"x", "name":"y"}' | ||
header(data, file, paths) | ||
f(data, file, paths) | ||
}) | ||
@@ -30,5 +32,7 @@ t.end() | ||
var filename = path.join(paths.data, 'index.md') | ||
, file = readFileSync(filename) | ||
, it = item(props, filename, file.toString()) | ||
var buf = readFileSync(filename) | ||
var str = new string_decoder.StringDecoder().write(buf) | ||
var it = blake.item(config, filename, str) | ||
var h = it.header | ||
@@ -45,8 +49,5 @@ t.equal(h.template, 'index.jade') | ||
t.same(it.date, h.date) | ||
t.equal(it.pubDate, strftime(h.date, '%a, %d %b %Y %T %z')) | ||
t.equal(it.dateString, h.date.toDateString()) | ||
t.ok(it.template instanceof Buffer, 'should be instance of Buffer') | ||
t.equal(it.title, h.title) | ||
t.equal(it.link, 'index.html') | ||
t.ok(typeof it.bake === 'function', 'should be function type') | ||
t.ok(typeof it.view === 'function', 'should be function type') | ||
t.equal(it.name, 'index.html') | ||
@@ -53,0 +54,0 @@ t.equal(it.path, path.join(target, h.name)) |
@@ -0,10 +1,14 @@ | ||
var blake = require('../') | ||
var common = require('./lib/common') | ||
var path = require('path') | ||
var test = require('tap').test | ||
, path = require('path') | ||
, read = require('../lib/read').readItems | ||
, config = require('./config') | ||
, props = config.props | ||
var source = common.source(__dirname) | ||
var target = common.freshTarget() | ||
var b = blake(source, target) | ||
test('read file', function (t) { | ||
var file = path.join(props.paths.data, 'index.md') | ||
read(props)(file, function (err, item) { | ||
var p = path.resolve(source, 'data', 'index.md') | ||
b.items(p, function (er, item) { | ||
if (er) throw er | ||
t.ok(item.header, 'should have header') | ||
@@ -17,3 +21,5 @@ t.ok(item.body, 'should have body') | ||
test('read directory', function (t) { | ||
read(props)(props.paths.data, function (err, items) { | ||
var p = path.resolve(source, 'data') | ||
b.items(p, function (er, items) { | ||
if (er) throw er | ||
items.forEach(function (item) { | ||
@@ -20,0 +26,0 @@ t.ok(item.header, 'should have header') |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
2
1
9
23877
18
555
215
1
+ Addedfstream@1.0.12(transitive)
+ Addedgraceful-fs@4.2.11(transitive)
+ Addedpopfun@1.0.0(transitive)
- Removedfstream@0.1.31(transitive)
- Removedgraceful-fs@3.0.12(transitive)
- Removedminimist@0.0.8(transitive)
- Removedmkdirp@0.4.2(transitive)
- Removednatives@1.1.6(transitive)
- Removedpopfun@0.1.2(transitive)
Updatedevent-stream@^3.3.1
Updatedfstream@^1.0.7
Updatedlru-cache@^2.6.5
Updatedmkdirp@^0.5.1
Updatedpopfun@^1.0.0