Merge pull request #25 from GitbookIO/feature/generators

Feature/generators
This commit is contained in:
Aaron O'Mullan
2014-04-04 10:59:13 -07:00
14 changed files with 272 additions and 188 deletions
+14 -10
View File
@@ -1,6 +1,7 @@
#! /usr/bin/env node
// Requires
var Q = require('q');
var _ = require('lodash');
var path = require('path');
var prog = require('commander');
@@ -10,9 +11,14 @@ var fs = require('fs');
var pkg = require('../package.json');
var generate = require("../lib/generate");
var parse = require("../lib/parse");
var generators = require("../lib/generate").generators;
var utils = require('./utils');
var logError = function(err) {
console.log(err.message || err);
return Q.reject(err);
};
// General options
prog
@@ -25,6 +31,7 @@ prog
.command('build [source_dir]')
.description('Build a gitbook from a directory')
.option('-o, --output <directory>', 'Path to output directory, defaults to ./_book')
.option('-g, --generator <name>', 'Change generator, defaults to site, availables are: '+_.keys(generators).join(", "))
.option('-t, --title <name>', 'Name of the book to generate, defaults to repo name')
.option('-i, --intro <intro>', 'Description of the book to generate')
.option('-g, --github <repo_path>', 'ID of github repo like : username/repo')
@@ -55,20 +62,19 @@ prog
var title = options.title || utils.titleCase(repo);
return generate.folder(
dir,
outputDir,
{
input: dir,
output: outputDir,
title: title,
description: options.intro,
github: githubID
github: githubID,
generator: options.generator
}
);
})
.then(function(output) {
console.log("Successfuly built !");
}, function(err) {
throw err;
})
}, logError)
.then(_.constant(outputDir));
});
@@ -85,15 +91,13 @@ prog
.then(function(outputDir) {
console.log();
console.log('Starting server ...');
return utils.serveDir(outputDir, options.port);
return utils.serveDir(outputDir, options.port)
.fail(logError);
})
.then(function() {
console.log('Serving book on http://localhost:'+options.port);
console.log();
console.log('Press CTRL+C to quit ...');
})
.fail(function(err) {
console.error(err);
});
});
+32
View File
@@ -0,0 +1,32 @@
var path = require("path");
var Q = require("q");
var fs = require("./fs");
var BaseGenerator = function(options) {
this.options = options;
};
BaseGenerator.prototype.convertFile = function(content, input) {
return Q.reject(new Error("Could not convert "+input));
};
BaseGenerator.prototype.transferFile = function(input) {
return fs.copy(
path.join(this.options.input, input),
path.join(this.options.output, input)
);
};
BaseGenerator.prototype.transferFolder = function(input) {
return fs.mkdirp(
path.join(this.options.output, input)
);
};
BaseGenerator.prototype.finish = function() {
return Q.reject(new Error("Could not finish generation"));
};
module.exports = BaseGenerator;
+47
View File
@@ -0,0 +1,47 @@
var util = require("util");
var path = require("path");
var Q = require("q");
var fs = require("./fs");
var parse = require("../parse");
var BaseGenerator = require("./generator");
var Generator = function() {
BaseGenerator.apply(this, arguments);
};
util.inherits(Generator, BaseGenerator);
Generator.prototype.transferFile = function(input) {
// ignore
};
Generator.prototype.convertFile = function(content, input) {
var that = this;
var json = {
progress: parse.progress(this.options.navigation, input)
};
return Q()
.then(function() {
return parse.page(content, {
repo: that.options.githubId,
dir: path.dirname(input) || '/'
});
})
.then(function(sections) {
json.sections = sections;
})
.then(function() {
return fs.writeFile(
path.join(that.options.output, input.replace(".md", ".json")),
JSON.stringify(json, null, 4)
);
});
};
Generator.prototype.finish = function() {
// ignore
};
module.exports = Generator;
+91
View File
@@ -0,0 +1,91 @@
var util = require("util");
var path = require("path");
var Q = require("q");
var swig = require('swig');
var fs = require("./fs");
var parse = require("../parse");
var BaseGenerator = require("./generator");
// Swig filter for returning the count of lines in a code section
swig.setFilter('lines', function(content) {
return content.split('\n').length;
});
// Swig filter for returning a link to the associated html file of a markdown file
swig.setFilter('mdLink', function(link) {
return link.replace(".md", ".html");
});
var Generator = function() {
BaseGenerator.apply(this, arguments);
// Load base template
this.template = swig.compileFile(path.resolve(__dirname, '../../templates/page.html'));
};
util.inherits(Generator, BaseGenerator);
// Convert a markdown file to html
Generator.prototype.convertFile = function(content, _input) {
var that = this;
var progress = parse.progress(this.options.navigation, _input);
_output = _input.replace(".md", ".html");
var input = path.join(this.options.input, _input);
var output = path.join(this.options.output, _output);
var basePath = path.relative(path.dirname(output), this.options.output) || ".";
return Q()
.then(function() {
return parse.page(content, {
repo: that.options.githubId,
dir: path.dirname(input) || '/'
});
})
.then(function(sections) {
return that.template({
title: that.options.title,
description: that.options.description,
githubAuthor: that.options.github.split("/")[0],
githubId: that.options.github,
githubHost: that.options.githubHost,
summary: that.options.summary,
allNavigation: that.options.navigation,
progress: progress,
_input: _input,
content: sections,
basePath: basePath,
staticBase: path.join(basePath, "gitbook"),
});
})
.then(function(html) {
return fs.writeFile(
output,
html
);
});
};
// Symlink index.html and copy assets
Generator.prototype.finish = function() {
var that = this;
return fs.symlink(
path.join(that.options.output, 'README.html'),
path.join(that.options.output, 'index.html')
)
.then(function() {
return fs.copy(
path.join(__dirname, "../../assets/static"),
path.join(that.options.output, "gitbook")
);
});
};
module.exports = Generator;
+40 -58
View File
@@ -5,12 +5,24 @@ var path = require("path");
var fs = require("./fs");
var parse = require("../parse");
var template = require("./template");
var generate = function(root, output, options) {
var files, summary, navigation, tpl;
var generators = {
"site": require("./generator_site"),
"json": require("./generator_json")
};
var generate = function(options) {
var generator = null;
var files;
options = _.defaults(options || {}, {
// Folders to use
input: null,
output: null,
// Output generator
generator: "site",
// Book title, keyword, description
title: null,
description: "Book generated using GitBook",
@@ -20,20 +32,24 @@ var generate = function(root, output, options) {
githubHost: 'https://github.com/'
});
if (!options.github || !options.title) {
return Q.reject(new Error("Need options.github and options.title"));
if (!options.github || !options.title || !options.input || !options.output) {
return Q.reject(new Error("Need options: github, title, input, output"));
}
if (!generators[options.generator]) {
return Q.reject(new Error("Invalid generator (availables are: "+_.keys(generators).join(", ")));
}
// Clean output folder
return fs.remove(output)
return fs.remove(options.output)
.then(function() {
return fs.mkdirp(output);
return fs.mkdirp(options.output);
})
// List all files in the repository
.then(function() {
return fs.list(root);
return fs.list(options.input);
})
// Check repository is valid
@@ -47,32 +63,18 @@ var generate = function(root, output, options) {
// Get summary
.then(function() {
return fs.readFile(path.join(root, "SUMMARY.md"), "utf-8")
return fs.readFile(path.join(options.input, "SUMMARY.md"), "utf-8")
.then(function(_summary) {
summary = parse.summary(_summary);
options.summary = parse.summary(_summary);
// Parse navigation
navigation = parse.navigation(summary);
options.navigation = parse.navigation(options.summary);
});
})
// Create template
// Create the generator
.then(function() {
tpl = template({
root: root,
output: output,
locals: {
title: options.title,
description: options.description,
githubAuthor: options.github.split("/")[0],
githubId: options.github,
githubHost: options.githubHost,
summary: summary,
allNavigation: navigation
}
});
generator = new generators[options.generator](options);
})
// Copy file and replace markdown file
@@ -81,49 +83,29 @@ var generate = function(root, output, options) {
_.chain(files)
.map(function(file) {
if (!file) return;
var _html = file.replace(".md", ".html");
// Folder
if (file[file.length -1] == "/") {
return fs.mkdirp(
path.join(output, file)
);
}
// Markdown file (only from the summary)
else if (path.extname(file) == ".md" && navigation[_html] != null) {
return tpl(file, file.replace(".md", ".html"));
}
// Copy file
else {
return fs.copy(
path.join(root, file),
path.join(output, file)
);
return Q(generator.transferFolder(file));
} else if (path.extname(file) == ".md" && options.navigation[file] != null) {
return fs.readFile(path.join(options.input, file), "utf-8")
.then(function(content) {
return Q(generator.convertFile(content, file));
});
} else {
return Q(generator.transferFile(file));
}
})
.value()
);
})
// Symlink index.html to README.html
// Finish gneration
.then(function() {
return fs.symlink(
path.join(output, 'README.html'),
path.join(output, 'index.html')
);
})
// Copy assets
.then(function() {
return fs.copy(
path.join(__dirname, "../../assets/static"),
path.join(output, "gitbook")
);
return generator.finish();
});
};
module.exports = {
generators: generators,
folder: generate
};
-66
View File
@@ -1,66 +0,0 @@
var swig = require('swig');
var path = require('path');
var _ = require("lodash");
var parse = require("../parse");
var fs = require('./fs');
swig.setFilter('lines', function(content) {
return content.split('\n').length;
});
// return a templeter for page
var initTemplate = function(options) {
var tpl = swig.compileFile(path.resolve(__dirname, '../../templates/page.html'));
options = _.defaults(options || {}, {
// Base folder for input
root: "./",
// Base folder for output
output: "./",
// Locals for templates
locals: {}
});
return function(input, output, local) {
var _input = input, _output = output;
input = path.join(options.root, input);
output = path.join(options.output, output);
var basePath = path.relative(path.dirname(output), options.output) || ".";
// Read markdown file
return fs.readFile(input, "utf-8")
// Parse sections
.then(function(markdown) {
return parse.page(markdown, {
repo: options.locals.githubId,
dir: path.dirname(_input) || '/'
});
})
//Calcul template
.then(function(sections) {
return tpl(
_.extend(local || {}, options.locals, {
_input: _input,
content: sections,
basePath: basePath,
staticBase: path.join(basePath, "gitbook"),
progress: parse.progress(options.locals.allNavigation, _output)
})
);
})
// Write html
.then(function(html) {
return fs.writeFile(output, html);
})
}
};
module.exports = initTemplate;
+5 -5
View File
@@ -1,15 +1,15 @@
var _ = require('lodash');
// Cleans up an article/chapter object
// remove 'articles' and '_path' attributes
// remove 'articles' attributes
function clean(obj) {
return obj && _.omit(obj, ['articles', '_path']);
return obj && _.omit(obj, ['articles']);
}
// Returns from a summary a map of
/*
{
"file/path.html": {
"file/path.md": {
prev: ...,
next: ...,
},
@@ -25,7 +25,7 @@ function navigation(summary, files) {
// Special README nav
var README_NAV = {
path: 'README.html',
path: 'README.md',
title: 'Introduction',
};
@@ -69,7 +69,7 @@ function navigation(summary, files) {
});
// Hack for README.html
mapping['README.html'] = {
mapping['README.md'] = {
title: README_NAV.title,
prev: null,
next: clean(summary.chapters[0]),
+1 -4
View File
@@ -82,10 +82,7 @@ function parseTitle(src, nums) {
level: level,
// Replace .md references with .html
path: matches[2].replace(/\.md$/, '.html'),
// Original, non normalized path
_path: matches[2],
path: matches[2],
};
}
+2 -2
View File
@@ -1,10 +1,10 @@
<div class="page-footer">
{% if _input == "README.md" %}
<a href="{{ basePath }}/{{ progress.current.next.path }}" class="navigation-link">Start</a>
<a href="{{ basePath }}/{{ progress.current.next.path|mdLink }}" class="navigation-link">Start</a>
{% else %}
{% if progress.current.next %}
{% if progress.current.next.path %}
<a href="{{ basePath }}/{{ progress.current.next.path }}" class="navigation-link next">Next</a>
<a href="{{ basePath }}/{{ progress.current.next.path|mdLink }}" class="navigation-link next">Next</a>
{% else %}
<div class="navigation-link coming-soon">Coming soon</div>
{% endif %}
+1 -1
View File
@@ -4,7 +4,7 @@
</div>
<div class="chapters">
{% for p in progress.chapters %}
<a href="{{ basePath }}/{{ p.path }}" title="{{ p.title }}" class="chapter {% if p.done %}done{% endif %} {% if p.level.length == 1 %}new-chapter{% endif %}" data-progress="{{ p.level }}" style="left: {{ p.percent }}%;"></a>
<a href="{{ basePath }}/{{ p.path|mdLink }}" title="{{ p.title }}" class="chapter {% if p.done %}done{% endif %} {% if p.level.length == 1 %}new-chapter{% endif %}" data-progress="{{ p.level }}" style="left: {{ p.percent }}%;"></a>
{% endfor %}
</div>
</div>
+2 -2
View File
@@ -16,7 +16,7 @@
{% for item in summary.chapters %}
<li {% if item._path == _input %}class="active"{% endif %} data-level="{{ item.level }}">
{% if item.path %}
<a href="{{ basePath }}/{{ item.path }}">
<a href="{{ basePath }}/{{ item.path|mdLink }}">
<i class="fa fa-check"></i> <b>{{ item.level }})</b> {{ item.title }}
</a>
{% else %}
@@ -27,7 +27,7 @@
{% for article in item.articles %}
<li {% if article._path == _input %}class="active"{% endif %} data-level="{{ article.level }}">
{% if article.path %}
<a href="{{ basePath }}/{{ article.path }}">
<a href="{{ basePath }}/{{ article.path|mdLink }}">
<i class="fa fa-check"></i> <b>{{ article.level }})</b> {{ article.title }}
</a>
{% else %}
+2 -2
View File
@@ -13,10 +13,10 @@
<meta name="generator" content="www.gitbook.io">
{% if progress.current.next and progress.current.next.path %}
<link rel="next" href="{{ basePath }}/{{ progress.current.next.path }}" />
<link rel="next" href="{{ basePath }}/{{ progress.current.next.path|mdLink }}" />
{% endif %}
{% if progress.current.prev and progress.current.prev.path %}
<link rel="prev" href="{{ basePath }}/{{ progress.current.prev.path }}" />
<link rel="prev" href="{{ basePath }}/{{ progress.current.prev.path|mdLink }}" />
{% endif %}
<meta property="og:title" content="{% block title %} | {{ title }}{% endblock %}">
+32 -32
View File
@@ -13,57 +13,57 @@ var LEXED = summary(CONTENT);
describe('Summary navigation', function() {
it('should provide next & prev entries for a file', function() {
var nav = navigation(LEXED, [
'README.html',
'chapter-1/README.html',
'chapter-1/ARTICLE1.html',
'chapter-1/ARTICLE2.html',
'chapter-2/README.html',
'README.md',
'chapter-1/README.md',
'chapter-1/ARTICLE1.md',
'chapter-1/ARTICLE2.md',
'chapter-2/README.md',
]);
// Make sure it found the files we gave it
assert(nav['README.html']);
assert(nav['chapter-1/README.html']);
assert(nav['chapter-1/ARTICLE1.html']);
assert(nav['chapter-1/ARTICLE2.html']);
assert(nav['chapter-2/README.html']);
assert(nav['README.md']);
assert(nav['chapter-1/README.md']);
assert(nav['chapter-1/ARTICLE1.md']);
assert(nav['chapter-1/ARTICLE2.md']);
assert(nav['chapter-2/README.md']);
assert.equal(nav['README.html'].prev, null);
assert.equal(nav['README.html'].next.path, 'chapter-1/README.html');
assert.equal(nav['README.md'].prev, null);
assert.equal(nav['README.md'].next.path, 'chapter-1/README.md');
assert.equal(nav['chapter-1/README.html'].prev.path, 'README.html');
assert.equal(nav['chapter-1/README.html'].next.path, 'chapter-1/ARTICLE1.html');
assert.equal(nav['chapter-1/README.md'].prev.path, 'README.md');
assert.equal(nav['chapter-1/README.md'].next.path, 'chapter-1/ARTICLE1.md');
assert.equal(nav['chapter-1/ARTICLE1.html'].prev.path, 'chapter-1/README.html');
assert.equal(nav['chapter-1/ARTICLE1.html'].next.path, 'chapter-1/ARTICLE2.html');
assert.equal(nav['chapter-1/ARTICLE1.md'].prev.path, 'chapter-1/README.md');
assert.equal(nav['chapter-1/ARTICLE1.md'].next.path, 'chapter-1/ARTICLE2.md');
assert.equal(nav['chapter-1/ARTICLE2.html'].prev.path, 'chapter-1/ARTICLE1.html');
assert.equal(nav['chapter-1/ARTICLE2.html'].next.path, 'chapter-2/README.html');
assert.equal(nav['chapter-1/ARTICLE2.md'].prev.path, 'chapter-1/ARTICLE1.md');
assert.equal(nav['chapter-1/ARTICLE2.md'].next.path, 'chapter-2/README.md');
assert.equal(nav['chapter-2/README.html'].prev.path, 'chapter-1/ARTICLE2.html');
assert.equal(nav['chapter-2/README.html'].next.path, 'chapter-3/README.html');
assert.equal(nav['chapter-2/README.md'].prev.path, 'chapter-1/ARTICLE2.md');
assert.equal(nav['chapter-2/README.md'].next.path, 'chapter-3/README.md');
});
it('should give full tree, when not limited', function() {
var nav = navigation(LEXED);
assert(nav['README.html']);
assert(nav['chapter-1/README.html']);
assert(nav['chapter-1/ARTICLE1.html']);
assert(nav['chapter-1/ARTICLE2.html']);
assert(nav['chapter-2/README.html']);
assert(nav['chapter-3/README.html']);
assert(nav['README.md']);
assert(nav['chapter-1/README.md']);
assert(nav['chapter-1/ARTICLE1.md']);
assert(nav['chapter-1/ARTICLE2.md']);
assert(nav['chapter-2/README.md']);
assert(nav['chapter-3/README.md']);
});
it('should detect levels correctly', function() {
var nav = navigation(LEXED);
assert.equal(nav['README.html'].level, '0');
assert.equal(nav['chapter-1/README.html'].level, '1');
assert.equal(nav['chapter-1/ARTICLE1.html'].level, '1.1');
assert.equal(nav['chapter-1/ARTICLE2.html'].level, '1.2');
assert.equal(nav['chapter-2/README.html'].level, '2');
assert.equal(nav['chapter-3/README.html'].level, '3');
assert.equal(nav['README.md'].level, '0');
assert.equal(nav['chapter-1/README.md'].level, '1');
assert.equal(nav['chapter-1/ARTICLE1.md'].level, '1.1');
assert.equal(nav['chapter-1/ARTICLE2.md'].level, '1.2');
assert.equal(nav['chapter-2/README.md'].level, '2');
assert.equal(nav['chapter-3/README.md'].level, '3');
});
it('should not accept null paths', function() {
+3 -6
View File
@@ -37,12 +37,9 @@ describe('Summary parsing', function () {
});
it('should normalize paths from .md to .html', function() {
assert.equal(LEXED.chapters[0].path,'chapter-1/README.html');
assert.equal(LEXED.chapters[0]._path,'chapter-1/README.md');
assert.equal(LEXED.chapters[1].path,'chapter-2/README.html');
assert.equal(LEXED.chapters[1]._path,'chapter-2/README.md');
assert.equal(LEXED.chapters[2].path,'chapter-3/README.html');
assert.equal(LEXED.chapters[2]._path,'chapter-3/README.md');
assert.equal(LEXED.chapters[0].path,'chapter-1/README.md');
assert.equal(LEXED.chapters[1].path,'chapter-2/README.md');
assert.equal(LEXED.chapters[2].path,'chapter-3/README.md');
});
it('should detect levels correctly', function() {