├── .jshintignore ├── .npmignore ├── .gitignore ├── screenshot.png ├── index.js ├── lib ├── ghost │ ├── helpers │ │ ├── tpl │ │ │ ├── nav.hbs │ │ │ └── pagination.hbs │ │ ├── template.js │ │ └── helpers.js │ ├── utils │ │ ├── pipeline.js │ │ └── downzero.js │ └── data │ │ └── schema.js ├── md │ ├── index.js │ ├── convert-md.js │ ├── parse-md.js │ ├── annotate-md-headings.js │ ├── highlight-js.js │ └── parse-header.js ├── stream │ ├── read.js │ ├── index.js │ ├── compute-url.js │ ├── dest.js │ ├── parse-tags.js │ ├── sort-by-published-at.js │ ├── file-to-post.js │ ├── group-by.js │ └── parse-published-at.js ├── theme.js ├── config-shim.js └── pipelines.js ├── Makefile ├── bin ├── default-config.json ├── usage.txt └── ghost-render ├── package.json ├── todo.md ├── reference.md ├── test └── metadata-parsing.test.js ├── readme.md └── public └── assets └── js └── jquery.min.js /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | screenshot.png 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | tmp/ 3 | settings.json 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixu/ghost-render/HEAD/screenshot.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | exports.Theme = require('./lib/theme'); 2 | exports.stream = require('./lib/stream'); 3 | exports.md = require('./lib/md'); 4 | exports.pipelines = require('./lib/pipelines'); 5 | -------------------------------------------------------------------------------- /lib/ghost/helpers/tpl/nav.hbs: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /lib/md/index.js: -------------------------------------------------------------------------------- 1 | exports.annotateMdHeadings = require('./annotate-md-headings'); 2 | exports.highlightJs = require('./highlight-js'); 3 | exports.parseHeader = require('./parse-header'); 4 | exports.parseMd = require('./parse-md'); 5 | exports.convertMd = require('./convert-md'); 6 | -------------------------------------------------------------------------------- /lib/md/convert-md.js: -------------------------------------------------------------------------------- 1 | var through = require('through2'), 2 | marked = require('marked'); 3 | 4 | function convertMd() { 5 | return through.obj(function(file, enc, onDone) { 6 | file.contents = marked.parser(file.contents); 7 | // push to next transform 8 | this.push(file); 9 | onDone(); 10 | }); 11 | } 12 | 13 | module.exports = convertMd; 14 | -------------------------------------------------------------------------------- /lib/md/parse-md.js: -------------------------------------------------------------------------------- 1 | var through = require('through2'), 2 | marked = require('marked'); 3 | 4 | function parseMd() { 5 | return through.obj(function(file, enc, onDone) { 6 | file.contents = marked.lexer(file.contents.toString()); 7 | // push to next transform 8 | this.push(file); 9 | onDone(); 10 | }); 11 | } 12 | 13 | module.exports = parseMd; 14 | -------------------------------------------------------------------------------- /lib/ghost/helpers/tpl/pagination.hbs: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GJSLINT := --nojsdoc --exclude_directories=node_modules,lib/require,test,temp --max_line_length=120 --disable=200,201,202,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,230,231,232,233,250,251,252 2 | 3 | build: 4 | rm -rf ./tmp/ 5 | node ./bin/ghost-render --settings settings.json 6 | 7 | lint: 8 | fixjsstyle $(GJSLINT) -r . 9 | gjslint $(GJSLINT) -r . 10 | jshint . 11 | 12 | .PHONY: lint build 13 | -------------------------------------------------------------------------------- /lib/stream/read.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | through = require('through2'); 3 | 4 | function read() { 5 | return through.obj(function(file, enc, onDone) { 6 | var stat = fs.statSync(file); 7 | if (stat.isFile()) { 8 | this.push({ 9 | path: file, 10 | stat: stat, 11 | contents: fs.readFileSync(file).toString() 12 | }); 13 | } 14 | onDone(); 15 | }); 16 | } 17 | 18 | module.exports = read; 19 | -------------------------------------------------------------------------------- /lib/stream/index.js: -------------------------------------------------------------------------------- 1 | exports.dest = require('./dest.js'); 2 | exports.read = require('./read.js'); 3 | exports.fileToPost = require('./file-to-post'); 4 | exports.parseTags = require('./parse-tags'); 5 | exports.parsePublishedAt = require('./parse-published-at'); 6 | exports.sortByPublishedAt = require('./sort-by-published-at'); 7 | exports.computeUrl = require('./compute-url'); 8 | 9 | var groupBy = require('./group-by'); 10 | exports.groupByAuthor = groupBy.author; 11 | exports.groupByTag = groupBy.tag; 12 | -------------------------------------------------------------------------------- /lib/md/annotate-md-headings.js: -------------------------------------------------------------------------------- 1 | var through = require('through2'); 2 | 3 | module.exports = function() { 4 | return through.obj(function annotateMarkdownHeadings(file, enc, onDone) { 5 | // file content is lexer output 6 | file.headings = file.contents.filter(function(token) { 7 | return token.type == 'heading'; 8 | }).map(function(token) { 9 | token.id = token.text.toLowerCase().replace(/[^a-z0-9]/g, '_'); 10 | return token; 11 | }); 12 | this.push(file); 13 | onDone(); 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /lib/stream/compute-url.js: -------------------------------------------------------------------------------- 1 | var path = require('path'), 2 | pi = require('pipe-iterators'); 3 | 4 | module.exports = function(inPath) { 5 | 6 | function computeUrl(val, file) { 7 | var relUrl = path.normalize(file.path.replace(inPath, '/')); 8 | // change the extension from .md to .html 9 | relUrl = path.dirname(relUrl) + '/' + path.basename(relUrl, path.extname(relUrl)) + '.html'; 10 | return relUrl; 11 | } 12 | 13 | return pi.mapKey({ 14 | relativeUrl: computeUrl, 15 | preComputedRelativeUrl: computeUrl 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /bin/default-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "blog": { 3 | "url": "http://localhost:5000", 4 | "title": "BLOG TITLE", 5 | "description": "BLOG DESCRIPTION", 6 | "logo": "http://lorempixel.com/45/45/abstract/", 7 | "cover": "http://lorempixel.com/1100/425/nature/" 8 | }, 9 | "authors": { 10 | "default": { 11 | "name": "AUTHOR NAME", 12 | "bio": "AUTHOR BIO", 13 | "website": "http://localhost:5000/author/foo", 14 | "image": "http://lorempixel.com/155/155/people/", 15 | "cover": "http://lorempixel.com/1100/425/animals/", 16 | "slug": "foo" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/ghost/utils/pipeline.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'); 2 | 3 | function pipeline(tasks /* initial arguments */) { 4 | var args = Array.prototype.slice.call(arguments, 1), 5 | 6 | runTask = function(task, args) { 7 | runTask = function(task, arg) { 8 | return task(arg); 9 | }; 10 | 11 | return task.apply(null, args); 12 | }; 13 | 14 | return Promise.all(tasks).reduce(function(arg, task) { 15 | return Promise.resolve(runTask(task, arg)).then(function(result) { 16 | return result; 17 | }); 18 | }, args); 19 | } 20 | 21 | module.exports = pipeline; 22 | -------------------------------------------------------------------------------- /lib/md/highlight-js.js: -------------------------------------------------------------------------------- 1 | var hljs = require('highlight.js'), 2 | through = require('through2'); 3 | 4 | 5 | function hl(code, lang) { 6 | return '
' + hljs.highlightAuto(code).value + '';
7 | }
8 |
9 | module.exports = function() {
10 | // code highlighting on lexer output
11 | return through.obj(function(file, enc, onDone) {
12 | file.contents.forEach(function(token, index) {
13 | if(token.type != 'code') {
14 | return;
15 | }
16 | file.contents[index] = { type: 'html', pre: false, text: hl(token.text, token.lang) };
17 | });
18 | this.push(file);
19 | onDone();
20 | });
21 | };
22 |
--------------------------------------------------------------------------------
/lib/stream/dest.js:
--------------------------------------------------------------------------------
1 | var path = require('path'),
2 | fs = require('fs'),
3 | through = require('through2'),
4 | mkdirp = require('mkdirp');
5 |
6 | function dest() {
7 | var seen = {};
8 |
9 | return through.obj(function(file, enc, onDone) {
10 | var writeDir = path.dirname(file.path);
11 | (seen[writeDir] ? function(a, onDone) { onDone(null); } : mkdirp)(
12 | writeDir, function(err) {
13 | if (err) {
14 | return onDone(err);
15 | }
16 | seen[writeDir] = true;
17 | fs.writeFileSync(file.path, file.contents);
18 | onDone();
19 | }
20 | );
21 | });
22 | }
23 |
24 | module.exports = dest;
25 |
--------------------------------------------------------------------------------
/lib/stream/parse-tags.js:
--------------------------------------------------------------------------------
1 | var pi = require('pipe-iterators');
2 |
3 | module.exports = function() {
4 | return pi.mapKey('tags', function(tags, file) {
5 | if (typeof tags !== 'string') {
6 | tags = '';
7 | }
8 |
9 | var splitBy = (tags.indexOf(',') > -1 ? ',' : ' '),
10 | parts = tags.split(splitBy).map(function(s) { return s.trim(); }).filter(Boolean);
11 |
12 | // post parsing must also set tags to an array!!
13 | return parts.map(function(name) { return {
14 | name: name,
15 | slug: name.toLowerCase().replace(/[^a-z0-9]/g, '-'),
16 | // needed for isTag
17 | description: name,
18 | parent: null
19 | }; });
20 | });
21 | };
22 |
--------------------------------------------------------------------------------
/lib/stream/sort-by-published-at.js:
--------------------------------------------------------------------------------
1 | var pi = require('pipe-iterators');
2 |
3 | module.exports = function() {
4 | var posts = [];
5 | return pi.through.obj(function(post, enc, onDone) {
6 | posts.push(post);
7 | onDone();
8 | }, function(onDone) {
9 | // sort posts
10 | posts.sort(function(a, b) {
11 | var aVal = (a.published_at ? a.published_at.getTime() : 0),
12 | bVal = (b.published_at ? b.published_at.getTime() : 0);
13 | return bVal - aVal; // newest first
14 | });
15 | // push each post back into the stream after sorting
16 | posts.forEach(function(post) {
17 | this.push(post);
18 | }, this);
19 | onDone();
20 | });
21 | };
22 |
--------------------------------------------------------------------------------
/lib/ghost/helpers/template.js:
--------------------------------------------------------------------------------
1 | var templates = {},
2 | hbs = require('express-hbs');
3 |
4 | // ## Template utils
5 |
6 | // Execute a template helper
7 | // All template helpers are register as partial view.
8 | templates.execute = function(name, context) {
9 | var partial = hbs.handlebars.partials[name];
10 |
11 | if (partial === undefined) {
12 | throw new Error('Template ' + name + ' not found.');
13 | return;
14 | }
15 |
16 | // If the partial view is not compiled, it compiles and saves in handlebars
17 | if (typeof partial === 'string') {
18 | hbs.registerPartial(partial);
19 | }
20 |
21 | return new hbs.handlebars.SafeString(partial(context));
22 | };
23 |
24 | module.exports = templates;
25 |
--------------------------------------------------------------------------------
/lib/ghost/data/schema.js:
--------------------------------------------------------------------------------
1 | function isPost(jsonData) {
2 | return jsonData.hasOwnProperty('html') && jsonData.hasOwnProperty('markdown') &&
3 | jsonData.hasOwnProperty('title') && jsonData.hasOwnProperty('slug');
4 | }
5 |
6 | function isTag(jsonData) {
7 | return jsonData.hasOwnProperty('name') && jsonData.hasOwnProperty('slug') &&
8 | jsonData.hasOwnProperty('description') && jsonData.hasOwnProperty('parent');
9 | }
10 |
11 | function isUser(jsonData) {
12 | return jsonData.hasOwnProperty('bio') && jsonData.hasOwnProperty('website') &&
13 | jsonData.hasOwnProperty('status') && jsonData.hasOwnProperty('location');
14 | }
15 |
16 | module.exports.checks = {
17 | isPost: isPost,
18 | isTag: isTag,
19 | isUser: isUser
20 | };
21 |
--------------------------------------------------------------------------------
/bin/usage.txt:
--------------------------------------------------------------------------------
1 | USAGE
2 |
3 | ghost-render --settings ./settings.json --theme ./path/to/theme
4 |
5 | REQUIRED
6 |
7 | --settings | t |