Skip to content

Commit

Permalink
Initial commit, it’s doing what it’s supposed to do.
Browse files Browse the repository at this point in the history
  • Loading branch information
zachleat committed Nov 27, 2017
0 parents commit 00ad919
Show file tree
Hide file tree
Showing 22 changed files with 436 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
@@ -0,0 +1,2 @@
dist/
node_modules/
34 changes: 34 additions & 0 deletions README.md
@@ -0,0 +1,34 @@
# elevenisland

Transform a directory of templates into HTML.

Works with:

* EJS (.ejs)
* Mustache (.mustache)
* Handlebars (.hbs)
* Markdown (.md)
* Haml (.haml)
* Pug (formerly Jade, .pug)
* Nunjucks (.njk)

## Usage

```
elevenisland
# Watch
elevenisland --watch
```

### Advanced

* Modify `data.json` to set global static data available to templates.
* Modify template format whitelist in `config.json`, the first one listed there is the default templating engine (default: `ejs`) and will be used to pre-process `data.json`.
* Markdown doesn’t render `data` and is also pre-processed using the default tempalting engine (default: `ejs`).

## Tests

```
npm run test
```
31 changes: 31 additions & 0 deletions cmd.js
@@ -0,0 +1,31 @@
#!/usr/bin/env node
const watch = require("glob-watcher");
const argv = require( "minimist" )( process.argv.slice(2) );
const TemplateWriter = require("./src/TemplateWriter");

const cfg = require("./config.json");
// argv._ ? argv._ :
const dir = cfg.dir.templates;

let writer = new TemplateWriter(
cfg.templateFormats.map(function(extension) {
return dir + "/*" + extension;
}),
cfg.dataFileName
);

writer.write();

if( argv.watch ) {
console.log( "Watching…" );
var watcher = watch(["./" + cfg.dir.templates + "/**", cfg.dataFileName]);
watcher.on("change", function(path, stat) {
console.log("File changed:", path);
writer.write();
});

watcher.on("add", function(path, stat) {
console.log("File added:", path);
writer.write();
});
}
8 changes: 8 additions & 0 deletions config.json
@@ -0,0 +1,8 @@
{
"dataFileName": "./data.json",
"templateFormats": ["ejs", "md", "hbs", "mustache", "haml", "pug", "njk"],
"dir": {
"templates": "templates",
"output": "dist"
}
}
5 changes: 5 additions & 0 deletions data.json
@@ -0,0 +1,5 @@
{
"title": "title",
"globaltitle": "This is a globaltitle.",
"version": "<%= _package.version %>"
}
39 changes: 39 additions & 0 deletions package.json
@@ -0,0 +1,39 @@
{
"name": "elevenisland",
"version": "1.0.0",
"description": "Transform a directory of templates into HTML.",
"main": "cmd.js",
"scripts": {
"default": "node cmd.js",
"watch": "node cmd.js --watch",
"test": "ava"
},
"author": {
"name": "Zach Leatherman",
"email": "zachleatherman@gmail.com",
"url": "https://zachleat.com/"
},
"bin": {
"elevenisland": "./cmd.js"
},
"devDependencies": {
"ava": "^0.23.0"
},
"dependencies": {
"consolidate": "^0.15.0",
"ejs": "^2.5.7",
"fs-extra": "^4.0.2",
"globby": "^7.1.1",
"glob-watcher": "^4.0.0",
"gray-matter": "^3.1.1",
"hamljs": "^0.6.2",
"handlebars": "^4.0.11",
"markdown-it": "^8.4.0",
"minimist": "^1.2.0",
"mustache": "^2.3.0",
"nunjucks": "^3.0.1",
"parse-filepath": "^1.0.1",
"promise": "^8.0.1",
"pug": "^2.0.0-rc.4"
}
}
30 changes: 30 additions & 0 deletions src/Layout.js
@@ -0,0 +1,30 @@
const CFG = require( "../config.json" );
const fs = require("fs-extra");

function Layout( name, dir ) {
this.dir = dir;
this.name = name;
this.filename = this.findFileName();
this.fullPath = this.dir + "/" + this.filename;
}

Layout.prototype.getFullPath = function() {
return this.fullPath;
};

Layout.prototype.findFileName = function() {
let file;
if( !fs.existsSync(this.dir) ) {
throw Error( "Layout directory does not exist: " + this.dir );
}
CFG.templateFormats.forEach(function( extension ) {
let filename = this.name + "." + extension;
if(!file && fs.existsSync( this.dir + "/" + filename)) {
file = filename;
}
}.bind(this));

return file;
};

module.exports = Layout;
73 changes: 73 additions & 0 deletions src/Template.js
@@ -0,0 +1,73 @@
const ejs = require("ejs");
const fs = require("fs-extra");
const parsePath = require('parse-filepath');
const matter = require('gray-matter');
const TemplateRender = require( "./TemplateRender" );
const Layout = require( "./Layout" );

const cfg = require("../config.json");

function Template( path, globalData ) {
this.path = path;
this.inputContent = this.getInput();
this.parsed = parsePath( path );
this.dir = this.parsed.dir.replace( new RegExp( "^" + cfg.dir.templates ), "" );
this.frontMatter = this.getMatter();
this.data = this.mergeData( globalData, this.frontMatter.data );
this.outputPath = this.getOutputPath();
}
Template.prototype.getOutputPath = function() {
return cfg.dir.output + "/" + ( this.dir ? this.dir + "/" : "" ) + this.parsed.name + ".html";
};
Template.prototype.getInput = function() {
return fs.readFileSync(this.path, "utf-8");
};
Template.prototype.getMatter = function() {
return matter( this.inputContent );
};
Template.prototype.mergeData = function( globalData, pageData ) {
let data = {};
for( let j in globalData ) {
data[ j ] = globalData[ j ];
}
for( let j in pageData ) {
data[ j ] = pageData[ j ];
}
return data;
};
Template.prototype.getPreRender = function() {
return this.frontMatter.content;
};
Template.prototype.renderLayout = function(tmpl, data) {
let layoutPath = (new Layout( tmpl.data.layout, cfg.dir.templates + "/_layouts" )).getFullPath();

console.log( "Reading layout " + tmpl.data.layout + ":", layoutPath );
let layout = new Template( layoutPath, {} );
let layoutData = this.mergeData( layout.data, data );
layoutData._layoutContent = this.renderContent( tmpl.getPreRender(), data );
let rendered = layout.renderContent( layout.getPreRender(), layoutData );
if( layout.data.layout ) {
return this.renderLayout( layout, layoutData );
}

return rendered;
};
Template.prototype.renderContent = function( str, data ) {
return ( new TemplateRender( this.path )).getRenderFunction()( str, data );
};
Template.prototype.render = function() {
if( this.data.layout ) {
return this.renderLayout(this, this.data);
} else {
return this.renderContent(this.getPreRender(), this.data);
}
};
Template.prototype.write = function() {
let err = fs.outputFileSync(this.outputPath, this.render());
if(err) {
throw err;
}
console.log( "Writing", this.outputPath );
};

module.exports = Template;
46 changes: 46 additions & 0 deletions src/TemplateRender.js
@@ -0,0 +1,46 @@
const parsePath = require('parse-filepath');
const ejs = require( "ejs" );
const md = require('markdown-it')();
const Handlebars = require('handlebars');
const Mustache = require('mustache');
const haml = require('hamljs');
const pug = require('pug');
const nunjucks = require('nunjucks');

function TemplateRender( path ) {
this.parsed = path ? parsePath( path ) : undefined;
}

TemplateRender.prototype.getRenderFunction = function() {
if( !this.parsed || this.parsed.ext === ".ejs" ) {
return ejs.render;
} else if( this.parsed.ext === ".md" ) {
return function(str, data) {
var render = (new TemplateRender()).getRenderFunction();
return md.render(render(str, data)).trim();
};
} else if( this.parsed.ext === ".hbs" ) {
return function(str, data) {
return Handlebars.compile(str)(data).trim();
}
} else if( this.parsed.ext === ".mustache" ) {
return function(str, data) {
return Mustache.render(str, data).trim();
}
} else if( this.parsed.ext === ".haml" ) {
return function(str, data) {
return haml.compile(str)(data).trim();
};
} else if( this.parsed.ext === ".pug" ) {
return function(str, data) {
return pug.compile(str)(data).trim();
};
} else if( this.parsed.ext === ".njk" ) {
return function(str, data) {
nunjucks.configure({ autoescape: false });
return nunjucks.renderString(str, data);
};
}
};

module.exports = TemplateRender;
36 changes: 36 additions & 0 deletions src/TemplateWriter.js
@@ -0,0 +1,36 @@
const fs = require("fs-extra");
const globby = require('globby');

const Template = require( "./Template" );
const TemplateRender = require( "./TemplateRender" );
const PKG = require("../package.json");

function TemplateWriter(files, globalDataPath) {
this.files = files;
this.globalRenderFunction = (new TemplateRender()).getRenderFunction();

this.globalDataPath = globalDataPath;
this.globalData = this.mergeDataImports(this.readJsonAsTemplate(globalDataPath));
}

TemplateWriter.prototype.mergeDataImports = function(data) {
data._package = PKG;
return data;
};

TemplateWriter.prototype.readJsonAsTemplate = function( path ) {
return JSON.parse( this.globalRenderFunction( fs.readFileSync( path, "utf-8" ), this.mergeDataImports({})));
};

TemplateWriter.prototype.write = function() {
let self = this;
globby(this.files).then(function(templates) {
templates.forEach(function(path) {
console.log( "Reading", path );
let tmpl = new Template( path, self.globalData );
tmpl.write();
});
});
};

module.exports = TemplateWriter;
11 changes: 11 additions & 0 deletions templates/_layouts/default.ejs
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %></title>
</head>
<body>
<%- _layoutContent %>
</body>
</html>
6 changes: 6 additions & 0 deletions templates/_layouts/post.ejs
@@ -0,0 +1,6 @@
---
layout: default
---
<div id="post">
<%- _layoutContent %>
</div>
7 changes: 7 additions & 0 deletions templates/_layouts/post2.ejs
@@ -0,0 +1,7 @@
---
layout: post
---
<div id="post2">
This is post2
<%- _layoutContent %>
</div>
20 changes: 20 additions & 0 deletions templates/test.ejs
@@ -0,0 +1,20 @@
---
title: 'Font Aliasing, or How to Rename a Font in CSS'
author: Zach Leatherman
layout: post2
permalink: /rename-font/
postRank: 4
daysPosted: 152
yearsPosted: 0.4
---

This is a teslkjdfklsjfkl kjsldjfkla

<h1><%= _package.version %></h1>
<h1><%= version %></h1>
<h1><%= title %></h1>

<%= globaltitle %>
<%= daysPosted %>
<%= permalink %>

20 changes: 20 additions & 0 deletions templates/test2.md
@@ -0,0 +1,20 @@
---
title: 'Font Aliasing, or How to Rename a Font in CSS'
permalink: /rename-font/
postRank: 4
daysPosted: 152
yearsPosted: 0.4
---

# This is a teslkjdfklsjfkl

## <%= _package.version %>

### <%= version %>

#### <%= title %>

<%= globaltitle %>
<%= daysPosted %>
<%= permalink %>

2 changes: 2 additions & 0 deletions templates/test3.hbs
@@ -0,0 +1,2 @@
<h1>{{_package.version}}</h1>
<p>{{globaltitle}}</p>
Empty file added test/LayoutStubs/default.ejs
Empty file.
Empty file added test/LayoutStubs/multiple.ejs
Empty file.
Empty file added test/LayoutStubs/multiple.md
Empty file.

0 comments on commit 00ad919

Please sign in to comment.