diff options
author | Aaron O'Mullan <aaron.omullan@gmail.com> | 2014-04-04 10:59:13 -0700 |
---|---|---|
committer | Aaron O'Mullan <aaron.omullan@gmail.com> | 2014-04-04 10:59:13 -0700 |
commit | d475affd3bb1255d82ff810bc8ef5ac8ac2f6f3a (patch) | |
tree | 060f11df6d7ccc24e222d35cf1ce2354f0fd6b02 | |
parent | fe3de93e06d5cc6fd757ee68013e1ec77f708018 (diff) | |
parent | 5cae311edf62e5c86edd9083a8d927730710c2f9 (diff) | |
download | gitbook-d475affd3bb1255d82ff810bc8ef5ac8ac2f6f3a.zip gitbook-d475affd3bb1255d82ff810bc8ef5ac8ac2f6f3a.tar.gz gitbook-d475affd3bb1255d82ff810bc8ef5ac8ac2f6f3a.tar.bz2 |
Merge pull request #25 from GitbookIO/feature/generators
Feature/generators
-rwxr-xr-x | bin/gitbook.js | 24 | ||||
-rw-r--r-- | lib/generate/generator.js | 32 | ||||
-rw-r--r-- | lib/generate/generator_json.js | 47 | ||||
-rw-r--r-- | lib/generate/generator_site.js | 91 | ||||
-rw-r--r-- | lib/generate/index.js | 98 | ||||
-rw-r--r-- | lib/generate/template.js | 66 | ||||
-rw-r--r-- | lib/parse/navigation.js | 10 | ||||
-rw-r--r-- | lib/parse/summary.js | 5 | ||||
-rw-r--r-- | templates/includes/book/footer.html | 4 | ||||
-rw-r--r-- | templates/includes/book/progress.html | 2 | ||||
-rw-r--r-- | templates/includes/book/summary.html | 4 | ||||
-rw-r--r-- | templates/layout.html | 4 | ||||
-rw-r--r-- | test/navigation.js | 64 | ||||
-rw-r--r-- | test/summary.js | 9 |
14 files changed, 272 insertions, 188 deletions
diff --git a/bin/gitbook.js b/bin/gitbook.js index 6e29986..3abeafd 100755 --- a/bin/gitbook.js +++ b/bin/gitbook.js @@ -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); }); }); diff --git a/lib/generate/generator.js b/lib/generate/generator.js new file mode 100644 index 0000000..3fe080e --- /dev/null +++ b/lib/generate/generator.js @@ -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;
\ No newline at end of file diff --git a/lib/generate/generator_json.js b/lib/generate/generator_json.js new file mode 100644 index 0000000..b331bfd --- /dev/null +++ b/lib/generate/generator_json.js @@ -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;
\ No newline at end of file diff --git a/lib/generate/generator_site.js b/lib/generate/generator_site.js new file mode 100644 index 0000000..7d40a1a --- /dev/null +++ b/lib/generate/generator_site.js @@ -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;
\ No newline at end of file diff --git a/lib/generate/index.js b/lib/generate/index.js index ee5ffc4..95f325c 100644 --- a/lib/generate/index.js +++ b/lib/generate/index.js @@ -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 - .then(function() { - return fs.symlink( - path.join(output, 'README.html'), - path.join(output, 'index.html') - ); - }) - - // Copy assets + // Finish gneration .then(function() { - return fs.copy( - path.join(__dirname, "../../assets/static"), - path.join(output, "gitbook") - ); + return generator.finish(); }); }; module.exports = { + generators: generators, folder: generate }; diff --git a/lib/generate/template.js b/lib/generate/template.js deleted file mode 100644 index d4e8cae..0000000 --- a/lib/generate/template.js +++ /dev/null @@ -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;
\ No newline at end of file diff --git a/lib/parse/navigation.js b/lib/parse/navigation.js index 145c873..bba25ce 100644 --- a/lib/parse/navigation.js +++ b/lib/parse/navigation.js @@ -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]), diff --git a/lib/parse/summary.js b/lib/parse/summary.js index 0995c40..7725b59 100644 --- a/lib/parse/summary.js +++ b/lib/parse/summary.js @@ -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], }; } diff --git a/templates/includes/book/footer.html b/templates/includes/book/footer.html index 48f4bf9..1112b14 100644 --- a/templates/includes/book/footer.html +++ b/templates/includes/book/footer.html @@ -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 %} diff --git a/templates/includes/book/progress.html b/templates/includes/book/progress.html index d0e3193..176c95f 100644 --- a/templates/includes/book/progress.html +++ b/templates/includes/book/progress.html @@ -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>
\ No newline at end of file diff --git a/templates/includes/book/summary.html b/templates/includes/book/summary.html index d74fb18..996ff3d 100644 --- a/templates/includes/book/summary.html +++ b/templates/includes/book/summary.html @@ -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 %} diff --git a/templates/layout.html b/templates/layout.html index c0f52cb..47184b3 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -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 %}"> diff --git a/test/navigation.js b/test/navigation.js index 23d98ac..b0e5b98 100644 --- a/test/navigation.js +++ b/test/navigation.js @@ -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() { diff --git a/test/summary.js b/test/summary.js index 51169d5..d1ef3dd 100644 --- a/test/summary.js +++ b/test/summary.js @@ -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() { |