Merge branch 'fixes'

This commit is contained in:
Samy Pesse
2016-04-30 20:15:08 +02:00
244 changed files with 9467 additions and 5025 deletions
+3 -1
View File
@@ -1,2 +1,4 @@
docs/**/*
test/node_modules/**/*
_book/**/*
node_modules/**/*
test/**/*
+3 -2
View File
@@ -12,8 +12,9 @@
},
"env": {
"node": true,
"mocha": true,
"browser": true
"browser": true,
"jest": true,
"jasmine": true
},
"extends": "eslint:recommended"
}
-1
View File
@@ -3,7 +3,6 @@ language: node_js
node_js:
- "stable"
- "4.1"
- "0.12"
before_install:
- npm install svgexport -g
after_success:
+2 -2
View File
@@ -5,8 +5,8 @@ init:
# Test against these versions of Node.js.
environment:
matrix:
- nodejs_version: "0.12"
- nodejs_version: "4.1"
- nodejs_version: "5"
- nodejs_version: "4"
# Install scripts. (runs after repo cloning)
install:
+2
View File
@@ -1,5 +1,7 @@
# GitBook Toolchain Documentation
![image](https://upload.wikimedia.org/wikipedia/commons/0/02/SVG_logo.svg)
This document aims to be a comprehensive guide to GitBook. It contains the full documentation for version **{{ book.version }}**. Help for GitBook.com specific questions can be found at [help.gitbook.com](https://help.gitbook.com).
### What is GitBook?
+25
View File
@@ -0,0 +1,25 @@
var path = require('path');
var fs = require('fs');
var matchers = {
/**
Verify that a file exists in a directory
*/
toHaveFile: function () {
return {
compare: function (actual, expected) {
var filePath = path.join(actual, expected);
var exists = fs.existsSync(filePath);
return {
pass: exists
};
}
};
}
};
jasmine.getEnv().beforeEach(function () {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000;
jasmine.addMatchers(matchers);
});
+9
View File
@@ -0,0 +1,9 @@
var gitbook = require('../gitbook');
describe('satisfies', function() {
it('should return true for *', function() {
expect(gitbook.satisfies('*')).toBe(true);
});
});
+16
View File
@@ -0,0 +1,16 @@
var tmp = require('tmp');
var initBook = require('../init');
describe('initBook', function() {
pit('should create a README and SUMMARY for empty book', function() {
var dir = tmp.dirSync();
return initBook(dir.name)
.then(function() {
expect(dir.name).toHaveFile('README.md');
expect(dir.name).toHaveFile('SUMMARY.md');
});
});
});
+6
View File
@@ -0,0 +1,6 @@
describe('GitBook', function() {
it('should correctly export', function() {
require('../');
});
});
+19
View File
@@ -0,0 +1,19 @@
var Config = require('../models/config');
/**
Decode changes from a JS API to a config object
@param {Config} config
@param {Object} result: result from API
@return {Config}
*/
function decodeGlobal(config, result) {
var values = result.values;
delete values.generator;
delete values.output;
return Config.updateValues(config, values);
}
module.exports = decodeGlobal;
+22
View File
@@ -0,0 +1,22 @@
var decodeConfig = require('./decodeConfig');
/**
Decode changes from a JS API to a output object.
Only the configuration can be edited by plugin's hooks
@param {Output} output
@param {Object} result: result from API
@return {Output}
*/
function decodeGlobal(output, result) {
var book = output.getBook();
var config = book.getConfig();
// Update config
config = decodeConfig(config, result.config);
book = book.set('config', config);
return output.set('book', book);
}
module.exports = decodeGlobal;
+44
View File
@@ -0,0 +1,44 @@
var deprecate = require('./deprecate');
/**
Decode changes from a JS API to a page object.
Only the content can be edited by plugin's hooks.
@param {Output} output
@param {Page} page: page instance to edit
@param {Object} result: result from API
@return {Page}
*/
function decodePage(output, page, result) {
var originalContent = page.getContent();
// No returned value
// Existing content will be used
if (!result) {
return page;
}
deprecate.disable('page.sections');
// GitBook 3
// Use returned page.content if different from original content
if (result.content != originalContent) {
page = page.set('content', result.content);
}
// GitBook 2 compatibility
// Finally, use page.sections
else if (result.sections) {
page = page.set('content',
result.sections.map(function(section) {
return section.content;
}).join('\n')
);
}
deprecate.enable('page.sections');
return page;
}
module.exports = decodePage;
+104
View File
@@ -0,0 +1,104 @@
var is = require('is');
var logged = {};
var disabled = {};
/**
Log a deprecated notice
@param {Book|Output} book
@param {String} key
@param {String} message
*/
function logNotice(book, key, message) {
if (logged[key] || disabled[key]) return;
logged[key] = true;
var logger = book.getLogger();
logger.warn.ln(message);
}
/**
Deprecate a function
@param {Book|Output} book
@param {String} key: unique identitifer for the deprecated
@param {Function} fn
@param {String} msg: message to print when called
@return {Function}
*/
function deprecateMethod(book, key, fn, msg) {
return function() {
logNotice(book, key, msg);
return fn.apply(this, arguments);
};
}
/**
Deprecate a property of an object
@param {Book|Output} book
@param {String} key: unique identitifer for the deprecated
@param {Object} instance
@param {String|Function} property
@param {String} msg: message to print when called
@return {Function}
*/
function deprecateField(book, key, instance, property, value, msg) {
var store = undefined;
var prepare = function() {
if (!is.undefined(store)) return;
if (is.fn(value)) store = value();
else store = value;
};
var getter = function(){
prepare();
logNotice(book, key, msg);
return store;
};
var setter = function(v) {
prepare();
logNotice(book, key, msg);
store = v;
return store;
};
Object.defineProperty(instance, property, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
/**
Enable a deprecation
@param {String} key: unique identitifer
*/
function enableDeprecation(key) {
disabled[key] = false;
}
/**
Disable a deprecation
@param {String} key: unique identitifer
*/
function disableDeprecation(key) {
disabled[key] = true;
}
module.exports = {
method: deprecateMethod,
field: deprecateField,
enable: enableDeprecation,
disable: disableDeprecation
};
+36
View File
@@ -0,0 +1,36 @@
var objectPath = require('object-path');
var deprecate = require('./deprecate');
/**
Encode a config object into a JS config api
@param {Output} output
@param {Config} config
@return {Object}
*/
function encodeConfig(output, config) {
var result = {
values: config.getValues().toJS(),
get: function(key, defaultValue) {
return objectPath.get(result.values, key, defaultValue);
},
set: function(key, value) {
return objectPath.set(result.values, key, value);
}
};
deprecate.field(output, 'config.options', result, 'options',
result.values, '"config.options" property is deprecated, use "config.get(key)" instead');
deprecate.field(output, 'config.options.generator', result.values, 'generator',
output.getGenerator(), '"options.generator" property is deprecated, use "output.name" instead');
deprecate.field(output, 'config.options.generator', result.values, 'output',
output.getRoot(), '"options.output" property is deprecated, use "output.root()" instead');
return result;
}
module.exports = encodeConfig;
+125
View File
@@ -0,0 +1,125 @@
var Promise = require('../utils/promise');
var PathUtils = require('../utils/path');
var fs = require('../utils/fs');
var deprecate = require('./deprecate');
var encodeConfig = require('./encodeConfig');
var encodeNavigation = require('./encodeNavigation');
var fileToURL = require('../output/helper/fileToURL');
/**
Encode a global context into a JS object
It's the context for page's hook, etc
@param {Output} output
@return {Object}
*/
function encodeGlobal(output) {
var book = output.getBook();
var bookFS = book.getContentFS();
var logger = output.getLogger();
var outputFolder = output.getRoot();
var result = {
log: logger,
config: encodeConfig(output, book.getConfig()),
isMultilingual: function() {
return book.isMultilingual();
},
isLanguageBook: function() {
return book.isLanguageBook();
},
isSubBook: deprecate.method(output, 'this.isSubBook', function() {
return book.isLanguageBook();
}, '"isSubBook" is deprecated, use "isLanguageBook()" instead'),
/**
Read a file from the book
@param {String} fileName
@return {Promise<Buffer>}
*/
readFile: function(fileName) {
return bookFS.read(fileName);
},
/**
Read a file from the book as a string
@param {String} fileName
@return {Promise<String>}
*/
readFileAsString: function(fileName) {
return bookFS.readAsString(fileName);
},
output: {
/**
Name of the generator being used
{String}
*/
name: output.getGenerator(),
/**
Return absolute path to the root folder of output
@return {String}
*/
root: function() {
return outputFolder;
},
/**
Convert a filepath into an url
@return {String}
*/
toURL: function(filePath) {
return fileToURL(output, filePath);
},
/**
Write a file to the output folder,
It creates the required folder
@param {String} fileName
@param {Buffer} content
@return {Promise}
*/
writeFile: function(fileName, content) {
return Promise()
.then(function() {
var filePath = PathUtils.resolveInRoot(outputFolder, fileName);
return fs.ensureFile(filePath)
.then(function() {
return fs.writeFile(filePath, content);
});
});
}
}
};
// todo
// template.applyBlock
// Deprecated properties
deprecate.field(output, 'this.generator', result, 'generator',
output.getGenerator(), '"this.generator" property is deprecated, use "this.output.name" instead');
deprecate.field(output, 'this.navigation', result, 'navigation', function() {
return encodeNavigation(output);
}, '"navigation" property is deprecated');
deprecate.field(output, 'this.book', result, 'book',
result, '"book" property is deprecated, use "this" directly instead');
deprecate.field(output, 'this.options', result, 'options',
result.config.values, '"options" property is deprecated, use config.get(key) instead');
return result;
}
module.exports = encodeGlobal;
+64
View File
@@ -0,0 +1,64 @@
var Immutable = require('immutable');
/**
Encode an article for next/prev
@param {Map<String:Page>}
@param {Article}
@return {Object}
*/
function encodeArticle(pages, article) {
var articlePath = article.getPath();
return {
path: articlePath,
title: article.getTitle(),
level: article.getLevel(),
exists: (articlePath && pages.has(articlePath)),
external: article.isExternal()
};
}
/**
this.navigation is a deprecated property from GitBook v2
@param {Output}
@return {Object}
*/
function encodeNavigation(output) {
var book = output.getBook();
var pages = output.getPages();
var summary = book.getSummary();
var articles = summary.getArticlesAsList();
var navigation = articles
.map(function(article, i) {
var ref = article.getRef();
if (!ref) {
return undefined;
}
var prev = articles.get(i - 1);
var next = articles.get(i + 1);
return [
ref,
{
index: i,
title: article.getTitle(),
introduction: (i === 0),
prev: prev? encodeArticle(pages, prev) : undefined,
next: next? encodeArticle(pages, next) : undefined,
level: article.getLevel()
}
];
})
.filter(function(e) {
return Boolean(e);
});
return Immutable.Map(navigation).toJS();
}
module.exports = encodeNavigation;
+39
View File
@@ -0,0 +1,39 @@
var JSONUtils = require('../json');
var deprecate = require('./deprecate');
var encodeProgress = require('./encodeProgress');
/**
Encode a page in a context to a JS API
@param {Output} output
@param {Page} page
@return {Object}
*/
function encodePage(output, page) {
var book = output.getBook();
var summary = book.getSummary();
var fs = book.getContentFS();
var file = page.getFile();
// JS Page is based on the JSON output
var result = JSONUtils.encodePage(page, summary);
result.type = file.getType();
result.path = file.getPath();
result.rawPath = fs.resolve(result.path);
deprecate.field(output, 'page.progress', result, 'progress', function() {
return encodeProgress(output, page);
}, '"page.progress" property is deprecated');
deprecate.field(output, 'page.sections', result, 'sections', [
{
content: result.content,
type: 'normal'
}
], '"sections" property is deprecated, use page.content instead');
return result;
}
module.exports = encodePage;
+63
View File
@@ -0,0 +1,63 @@
var Immutable = require('immutable');
var encodeNavigation = require('./encodeNavigation');
/**
page.progress is a deprecated property from GitBook v2
@param {Output}
@param {Page}
@return {Object}
*/
function encodeProgress(output, page) {
var current = page.getPath();
var navigation = encodeNavigation(output);
navigation = Immutable.Map(navigation);
var n = navigation.size;
var percent = 0, prevPercent = 0, currentChapter = null;
var done = true;
var chapters = navigation
.map(function(nav, chapterPath) {
nav.path = chapterPath;
return nav;
})
.valueSeq()
.sortBy(function(nav) {
return nav.index;
})
.map(function(nav, i) {
// Calcul percent
nav.percent = (i * 100) / Math.max((n - 1), 1);
// Is it done
nav.done = done;
if (nav.path == current) {
currentChapter = nav;
percent = nav.percent;
done = false;
} else if (done) {
prevPercent = nav.percent;
}
return nav;
})
.toJS();
return {
// Previous percent
prevPercent: prevPercent,
// Current percent
percent: percent,
// List of chapter with progress
chapters: chapters,
// Current chapter
current: currentChapter
};
}
module.exports = encodeProgress;
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
encodePage: require('./encodePage'),
decodePage: require('./decodePage'),
encodeGlobal: require('./encodeGlobal'),
decodeGlobal: require('./decodeGlobal')
};
-69
View File
@@ -1,69 +0,0 @@
var _ = require('lodash');
function BackboneFile(book) {
if (!(this instanceof BackboneFile)) return new BackboneFile(book);
this.book = book;
this.log = this.book.log;
// Filename in the book
this.path = '';
this.parser;
_.bindAll(this);
}
// Type of the backbone file
BackboneFile.prototype.type = '';
// Parse a backbone file
BackboneFile.prototype.parse = function() {
// To be implemented by each child
};
// Handle case where file doesn't exists
BackboneFile.prototype.parseNotFound = function() {
};
// Return true if backbone file exists
BackboneFile.prototype.exists = function() {
return Boolean(this.path);
};
// Locate a backbone file, could be .md, .asciidoc, etc
BackboneFile.prototype.locate = function() {
var that = this;
var filename = this.book.config.getStructure(this.type, true);
this.log.debug.ln('locating', this.type, ':', filename);
return this.book.findParsableFile(filename)
.then(function(result) {
if (!result) return;
that.path = result.path;
that.parser = result.parser;
});
};
// Read and parse the file
BackboneFile.prototype.load = function() {
var that = this;
this.log.debug.ln('loading', this.type, ':', that.path);
return this.locate()
.then(function() {
if (!that.path) return that.parseNotFound();
that.log.debug.ln(that.type, 'located at', that.path);
return that.book.readFile(that.path)
// Parse it
.then(function(content) {
return that.parse(content);
});
});
};
module.exports = BackboneFile;
-99
View File
@@ -1,99 +0,0 @@
var _ = require('lodash');
var util = require('util');
var BackboneFile = require('./file');
// Normalize a glossary entry name into a unique id
function nameToId(name) {
return name.toLowerCase()
.replace(/[\/\\\?\%\*\:\;\|\"\'\\<\\>\#\$\(\)\!\.\@]/g, '')
.replace(/ /g, '_')
.trim();
}
/*
A glossary entry is represented by a name and a short description
An unique id for the entry is generated using its name
*/
function GlossaryEntry(name, description) {
if (!(this instanceof GlossaryEntry)) return new GlossaryEntry(name, description);
this.name = name;
this.description = description;
Object.defineProperty(this, 'id', {
get: _.bind(this.getId, this)
});
}
// Normalizes a glossary entry's name to create an ID
GlossaryEntry.prototype.getId = function() {
return nameToId(this.name);
};
/*
A glossary is a list of entries stored in a GLOSSARY.md file
*/
function Glossary() {
BackboneFile.apply(this, arguments);
this.entries = [];
}
util.inherits(Glossary, BackboneFile);
Glossary.prototype.type = 'glossary';
// Get templating context
Glossary.prototype.getContext = function() {
if (!this.path) return {};
return {
glossary: {
path: this.path
}
};
};
// Parse the readme content
Glossary.prototype.parse = function(content) {
var that = this;
return this.parser.glossary(content)
.then(function(entries) {
that.entries = _.map(entries, function(entry) {
return new GlossaryEntry(entry.name, entry.description);
});
});
};
// Return an entry by its id
Glossary.prototype.get = function(id) {
return _.find(this.entries, {
id: id
});
};
// Find an entry by its name
Glossary.prototype.find = function(name) {
return this.get(nameToId(name));
};
// Return false if glossary has entries (and exists)
Glossary.prototype.isEmpty = function(id) {
return _.size(this.entries) === 0;
};
// Convert the glossary to a list of annotations
Glossary.prototype.annotations = function() {
return _.map(this.entries, function(entry) {
return {
id: entry.id,
name: entry.name,
description: entry.description,
href: '/' + this.path + '#' + entry.id
};
}, this);
};
module.exports = Glossary;
-8
View File
@@ -1,8 +0,0 @@
module.exports = {
Readme: require('./readme'),
Summary: require('./summary'),
Glossary: require('./glossary'),
Langs: require('./langs')
};
-81
View File
@@ -1,81 +0,0 @@
var _ = require('lodash');
var path = require('path');
var util = require('util');
var BackboneFile = require('./file');
function Language(title, folder) {
var that = this;
this.title = title;
this.folder = folder;
Object.defineProperty(this, 'id', {
get: function() {
return path.basename(that.folder);
}
});
}
/*
A Langs is a list of languages stored in a LANGS.md file
*/
function Langs() {
BackboneFile.apply(this, arguments);
this.languages = [];
}
util.inherits(Langs, BackboneFile);
Langs.prototype.type = 'langs';
// Parse the readme content
Langs.prototype.parse = function(content) {
var that = this;
return this.parser.langs(content)
.then(function(langs) {
that.languages = _.map(langs, function(entry) {
return new Language(entry.title, entry.path);
});
});
};
// Return the list of languages
Langs.prototype.list = function() {
return this.languages;
};
// Return default/main language for the book
Langs.prototype.getDefault = function() {
return _.first(this.languages);
};
// Return true if a language is the default one
// "lang" cam be a string (id) or a Language entry
Langs.prototype.isDefault = function(lang) {
lang = lang.id || lang;
return (this.cound() > 0 && this.getDefault().id == lang);
};
// Return the count of languages
Langs.prototype.count = function() {
return _.size(this.languages);
};
// Return templating context for the languages list
Langs.prototype.getContext = function() {
if (this.count() == 0) return {};
return {
languages: {
list: _.map(this.languages, function(lang) {
return {
id: lang.id,
title: lang.title
};
})
}
};
};
module.exports = Langs;
-44
View File
@@ -1,44 +0,0 @@
var util = require('util');
var BackboneFile = require('./file');
function Readme() {
BackboneFile.apply(this, arguments);
this.title;
this.description;
}
util.inherits(Readme, BackboneFile);
Readme.prototype.type = 'readme';
/*
Return and extension of context to define the readme
@retrun {Object}
*/
Readme.prototype.getContext = function() {
return {
readme: {
path: this.path
}
};
};
/*
Parse the readme content
@param {String} content
@retrun {Promise}
*/
Readme.prototype.parse = function(content) {
var that = this;
return this.parser.readme(content)
.then(function(out) {
that.title = out.title;
that.description = out.description;
});
};
module.exports = Readme;
-349
View File
@@ -1,349 +0,0 @@
var _ = require('lodash');
var util = require('util');
var location = require('../utils/location');
var error = require('../utils/error');
var BackboneFile = require('./file');
/*
An article represent an entry in the Summary.
It's defined by a title, a reference, and children articles,
the reference (ref) can be a filename + anchor or an external file (optional)
*/
function TOCArticle(def, parent) {
// Title
this.title = def.title;
// Parent TOCPart or TOCArticle
this.parent = parent;
// As string indicating the overall position
// ex: '1.0.0'
this.level;
this._next;
this._prev;
// When README has been automatically added
this.isAutoIntro = def.isAutoIntro;
this.isIntroduction = def.isIntroduction;
this.validate();
// Path can be a relative path or an url, or nothing
this.ref = def.path;
if (this.ref && !this.isExternal()) {
var parts = this.ref.split('#');
this.path = (parts.length > 1? parts.slice(0, -1).join('#') : this.ref);
this.anchor = (parts.length > 1? '#' + _.last(parts) : null);
// Normalize path to remove ('./', etc)
this.path = location.normalize(this.path);
}
this.articles = _.map(def.articles || [], function(article) {
if (article instanceof TOCArticle) return article;
return new TOCArticle(article, this);
}, this);
}
// Validate the article
TOCArticle.prototype.validate = function() {
if (!this.title) {
throw error.ParsingError(new Error('SUMMARY entries should have an non-empty title'));
}
};
// Iterate over all articles in this articles
TOCArticle.prototype.walk = function(iter, base) {
base = base || this.level;
_.each(this.articles, function(article, i) {
var level = levelId(base, i);
if (iter(article, level) === false) {
return false;
}
article.walk(iter, level);
});
};
// Return templating context for an article
TOCArticle.prototype.getContext = function() {
return {
level: this.level,
title: this.title,
depth: this.depth(),
path: this.isExternal()? undefined : this.path,
anchor: this.isExternal()? undefined : this.anchor,
url: this.isExternal()? this.ref : undefined
};
};
// Return true if is pointing to a file
TOCArticle.prototype.hasLocation = function() {
return Boolean(this.path);
};
// Return true if is pointing to an external location
TOCArticle.prototype.isExternal = function() {
return location.isExternal(this.ref);
};
// Return true if this article is the introduction
TOCArticle.prototype.isIntro = function() {
return Boolean(this.isIntroduction);
};
// Return true if has children
TOCArticle.prototype.hasChildren = function() {
return this.articles.length > 0;
};
// Return true if has an article as parent
TOCArticle.prototype.hasParent = function() {
return !(this.parent instanceof TOCPart);
};
// Return depth of this article
TOCArticle.prototype.depth = function() {
return this.level.split('.').length;
};
// Return next article in the TOC
TOCArticle.prototype.next = function() {
return this._next;
};
// Return previous article in the TOC
TOCArticle.prototype.prev = function() {
return this._prev;
};
// Map over all articles
TOCArticle.prototype.map = function(iter) {
return _.map(this.articles, iter);
};
/*
A part of a ToC is a composed of a tree of articles and an optiona title
*/
function TOCPart(part, parent) {
if (!(this instanceof TOCPart)) return new TOCPart(part, parent);
TOCArticle.apply(this, arguments);
}
util.inherits(TOCPart, TOCArticle);
// Validate the part
TOCPart.prototype.validate = function() { };
// Return a sibling (next or prev) of this part
TOCPart.prototype.sibling = function(direction) {
var parts = this.parent.parts;
var pos = _.findIndex(parts, this);
if (parts[pos + direction]) {
return parts[pos + direction];
}
return null;
};
// Iterate over all entries of the part
TOCPart.prototype.walk = function(iter, base) {
var articles = this.articles;
if (articles.length == 0) return;
// Has introduction?
if (articles[0].isIntro()) {
if (iter(articles[0], '0') === false) {
return;
}
articles = articles.slice(1);
}
_.each(articles, function(article, i) {
var level = levelId(base, i);
if (iter(article, level) === false) {
return false;
}
article.walk(iter, level);
});
};
// Return templating context for a part
TOCPart.prototype.getContext = function(onArticle) {
onArticle = onArticle || function(article) {
return article.getContext();
};
return {
title: this.title,
articles: this.map(onArticle)
};
};
/*
A summary is composed of a list of parts, each composed wit a tree of articles.
*/
function Summary() {
BackboneFile.apply(this, arguments);
this.parts = [];
this._length = 0;
}
util.inherits(Summary, BackboneFile);
Summary.prototype.type = 'summary';
// Prepare summary when non existant
Summary.prototype.parseNotFound = function() {
this.update([]);
};
// Parse the summary content
Summary.prototype.parse = function(content) {
var that = this;
return this.parser.summary(content)
.then(function(summary) {
that.update(summary.parts);
});
};
// Return templating context for the summary
Summary.prototype.getContext = function() {
function onArticle(article) {
var result = article.getContext();
if (article.hasChildren()) {
result.articles = article.map(onArticle);
}
return result;
}
return {
summary: {
path: this.path,
parts: _.map(this.parts, function(part) {
return part.getContext(onArticle);
})
}
};
};
// Iterate over all entries of the summary
// iter is called with an TOCArticle
Summary.prototype.walk = function(iter) {
var hasMultipleParts = this.parts.length > 1;
_.each(this.parts, function(part, i) {
part.walk(iter, hasMultipleParts? levelId('', i) : null);
});
};
// Find a specific article using a filter
Summary.prototype.find = function(filter) {
var result;
this.walk(function(article) {
if (filter(article)) {
result = article;
return false;
}
});
return result;
};
// Flatten the list of articles
Summary.prototype.flatten = function() {
var result = [];
this.walk(function(article) {
result.push(article);
});
return result;
};
// Return the first TOCArticle for a specific page (or path)
Summary.prototype.getArticle = function(page) {
if (!_.isString(page)) page = page.path;
return this.find(function(article) {
return article.path == page;
});
};
// Return the first TOCArticle for a specific level
Summary.prototype.getArticleByLevel = function(lvl) {
return this.find(function(article) {
return article.level == lvl;
});
};
// Return the count of articles in the summary
Summary.prototype.count = function() {
return this._length;
};
// Prepare the summary
Summary.prototype.update = function(parts) {
var that = this;
that.parts = _.map(parts, function(part) {
return new TOCPart(part, that);
});
// Create first part if none
if (that.parts.length == 0) {
that.parts.push(new TOCPart({}, that));
}
// Add README as first entry
var firstArticle = that.parts[0].articles[0];
if (!firstArticle || firstArticle.path != that.book.readme.path) {
that.parts[0].articles.unshift(new TOCArticle({
title: 'Introduction',
path: that.book.readme.path,
isAutoIntro: true
}, that.parts[0]));
}
that.parts[0].articles[0].isIntroduction = true;
// Update the count and indexing of "level"
var prev = undefined;
that._length = 0;
that.walk(function(article, level) {
// Index level
article.level = level;
// Chain articles
article._prev = prev;
if (prev) prev._next = article;
prev = article;
that._length += 1;
});
};
// Return a level string from a base level and an index
function levelId(base, i) {
i = i + 1;
return (base? [base || '', i] : [i]).join('.');
}
module.exports = Summary;
-396
View File
@@ -1,396 +0,0 @@
var _ = require('lodash');
var path = require('path');
var Ignore = require('ignore');
var Config = require('./config');
var Readme = require('./backbone/readme');
var Glossary = require('./backbone/glossary');
var Summary = require('./backbone/summary');
var Langs = require('./backbone/langs');
var Page = require('./page');
var pathUtil = require('./utils/path');
var error = require('./utils/error');
var Promise = require('./utils/promise');
var Logger = require('./utils/logger');
var parsers = require('./parsers');
var initBook = require('./init');
/*
The Book class is an interface for parsing books content.
It does not require to run on Node.js, isnce it only depends on the fs implementation
*/
function Book(opts) {
if (!(this instanceof Book)) return new Book(opts);
this.opts = _.defaults(opts || {}, {
fs: null,
// Root path for the book
root: '',
// Extend book configuration
config: {},
// Log function
log: function(msg) {
process.stdout.write(msg);
},
// Log level
logLevel: 'info'
});
if (!opts.fs) throw error.ParsingError(new Error('Book requires a fs instance'));
// Root path for the book
this.root = opts.root;
// If multi-lingual, book can have a parent
this.parent = opts.parent;
if (this.parent) {
this.language = path.relative(this.parent.root, this.root);
}
// A book is linked to an fs, to access its content
this.fs = opts.fs;
// Rules to ignore some files
this.ignore = Ignore();
this.ignore.addPattern([
// Skip Git stuff
'.git/',
// Skip OS X meta data
'.DS_Store',
// Skip stuff installed by plugins
'node_modules',
// Skip book outputs
'_book',
'*.pdf',
'*.epub',
'*.mobi'
]);
// Create a logger for the book
this.log = new Logger(opts.log, opts.logLevel);
// Create an interface to access the configuration
this.config = new Config(this, opts.config);
// Interfaces for the book structure
this.readme = new Readme(this);
this.summary = new Summary(this);
this.glossary = new Glossary(this);
// Multilinguals book
this.langs = new Langs(this);
this.books = [];
// List of page in the book
this.pages = {};
// Deprecation for templates
Object.defineProperty(this, 'options', {
get: function () {
this.log.warn.ln('"options" property is deprecated, use config.get(key) instead');
return this.config.options;
}
});
_.bindAll(this);
// Loop for template filters/blocks
error.deprecateField(this, 'book', this, '"book" property is deprecated, use "this" directly instead');
}
// Return templating context for the book
Book.prototype.getContext = function() {
var variables = this.config.get('variables', {});
return {
book: _.extend({
language: this.language
}, variables)
};
};
// Parse and prepare the configuration, fail if invalid
Book.prototype.prepareConfig = function() {
var that = this;
return this.config.load()
.then(function() {
var rootFolder = that.config.get('root');
if (!rootFolder) return;
that.originalRoot = that.root;
that.root = path.resolve(that.root, rootFolder);
});
};
// Resolve a path in the book source
// Enforce that the output path is in the scope
Book.prototype.resolve = function() {
var filename = path.resolve.apply(path, [this.root].concat(_.toArray(arguments)));
if (!this.isFileInScope(filename)) {
throw error.FileOutOfScopeError({
filename: filename,
root: this.root
});
}
return filename;
};
// Return false if a file is outside the book' scope
Book.prototype.isFileInScope = function(filename) {
filename = path.resolve(this.root, filename);
// Is the file in the scope of the parent?
if (this.parent && this.parent.isFileInScope(filename)) return true;
// Is file in the root folder?
return pathUtil.isInRoot(this.root, filename);
};
// Parse .gitignore, etc to extract rules
Book.prototype.parseIgnoreRules = function() {
var that = this;
return Promise.serie([
'.ignore',
'.gitignore',
'.bookignore'
], function(filename) {
return that.readFile(filename)
.then(function(content) {
that.ignore.addPattern(content.toString().split(/\r?\n/));
}, function() {
return Promise();
});
});
};
// Parse the whole book
Book.prototype.parse = function() {
var that = this;
return Promise()
.then(this.prepareConfig)
.then(this.parseIgnoreRules)
// Parse languages
.then(function() {
return that.langs.load();
})
.then(function() {
if (that.isMultilingual()) {
if (that.isLanguageBook()) {
throw error.ParsingError(new Error('A multilingual book as a language book is forbidden'));
}
that.log.info.ln('Parsing multilingual book, with', that.langs.count(), 'languages');
// Create a new book for each language and parse it
return Promise.serie(that.langs.list(), function(lang) {
that.log.debug.ln('Preparing book for language', lang.id);
var langBook = new Book(_.extend({}, that.opts, {
parent: that,
config: that.config.dump(),
root: that.resolve(lang.id)
}));
that.books.push(langBook);
return langBook.parse();
});
}
return Promise()
// Parse the readme
.then(that.readme.load)
.then(function() {
if (!that.readme.exists()) {
throw new error.FileNotFoundError({ filename: 'README' });
}
// Default configuration to infos extracted from readme
if (!that.config.get('title')) that.config.set('title', that.readme.title);
if (!that.config.get('description')) that.config.set('description', that.readme.description);
})
// Parse the summary
.then(that.summary.load)
.then(function() {
if (!that.summary.exists()) {
that.log.warn.ln('no summary file in this book');
}
// Index summary's articles
that.summary.walk(function(article) {
if (!article.hasLocation() || article.isExternal()) return;
that.addPage(article.path);
});
})
// Parse the glossary
.then(that.glossary.load)
// Add the glossary as a page
.then(function() {
if (!that.glossary.exists()) return;
that.addPage(that.glossary.path);
});
});
};
// Mark a filename as being parsable
Book.prototype.addPage = function(filename) {
if (this.hasPage(filename)) return this.getPage(filename);
filename = pathUtil.normalize(filename);
this.pages[filename] = new Page(this, filename);
return this.pages[filename];
};
// Return a page by its filename (or undefined)
Book.prototype.getPage = function(filename) {
filename = pathUtil.normalize(filename);
return this.pages[filename];
};
// Return true, if has a specific page
Book.prototype.hasPage = function(filename) {
return Boolean(this.getPage(filename));
};
// Test if a file is ignored, return true if it is
Book.prototype.isFileIgnored = function(filename) {
return this.ignore.filter([filename]).length == 0;
};
// Read a file in the book, throw error if ignored
Book.prototype.readFile = function(filename) {
if (this.isFileIgnored(filename)) return Promise.reject(new error.FileNotFoundError({ filename: filename }));
return this.fs.readAsString(this.resolve(filename));
};
// Get stat infos about a file
Book.prototype.statFile = function(filename) {
if (this.isFileIgnored(filename)) return Promise.reject(new error.FileNotFoundError({ filename: filename }));
return this.fs.stat(this.resolve(filename));
};
// Find a parsable file using a filename
Book.prototype.findParsableFile = function(filename) {
var that = this;
var ext = path.extname(filename);
var basename = path.basename(filename, ext);
// Ordered list of extensions to test
var exts = parsers.extensions;
if (ext) exts = _.uniq([ext].concat(exts));
return _.reduce(exts, function(prev, ext) {
return prev.then(function(output) {
// Stop if already find a parser
if (output) return output;
var filepath = basename+ext;
return that.fs.findFile(that.root, filepath)
.then(function(realFilepath) {
if (!realFilepath) return null;
return {
parser: parsers.getByExt(ext),
path: realFilepath
};
});
});
}, Promise(null));
};
// Return true if book is associated to a language
Book.prototype.isLanguageBook = function() {
return Boolean(this.parent);
};
Book.prototype.isSubBook = Book.prototype.isLanguageBook;
// Return true if the book is main instance of a multilingual book
Book.prototype.isMultilingual = function() {
return this.langs.count() > 0;
};
// Return true if file is in the scope of this book
Book.prototype.isInBook = function(filename) {
return pathUtil.isInRoot(
this.root,
filename
);
};
// Return true if file is in the scope of a child book
Book.prototype.isInLanguageBook = function(filename) {
var that = this;
return _.some(this.langs.list(), function(lang) {
return pathUtil.isInRoot(
that.resolve(lang.id),
that.resolve(filename)
);
});
};
// ----- Parser Methods
// Render a markup string in inline mode
Book.prototype.renderInline = function(type, src) {
var parser = parsers.get(type);
return parser.inline(src)
.get('content');
};
// Render a markup string in block mode
Book.prototype.renderBlock = function(type, src) {
var parser = parsers.get(type);
return parser.page(src)
.get('content');
};
// ----- DEPRECATED METHODS
Book.prototype.contentLink = error.deprecateMethod(function(s) {
return this.output.toURL(s);
}, '.contentLink() is deprecated, use ".output.toURL()" instead');
Book.prototype.contentPath = error.deprecateMethod(function(s) {
return this.output.toURL(s);
}, '.contentPath() is deprecated, use ".output.toURL()" instead');
Book.prototype.isSubBook = error.deprecateMethod(function() {
return this.isLanguageBook();
}, '.isSubBook() is deprecated, use ".isLanguageBook()" instead');
// Initialize a book
Book.init = function(fs, root, opts) {
var book = new Book(_.extend(opts || {}, {
root: root,
fs: fs
}));
return initBook(book);
};
module.exports = Book;
+14
View File
@@ -0,0 +1,14 @@
var Modifiers = require('./modifiers');
module.exports = {
Parse: require('./parse'),
// Models
Book: require('./models/book'),
FS: require('./models/fs'),
Summary: require('./models/summary'),
Glossary: require('./models/glossary'),
// Modifiers
SummaryModifier: Modifiers.Summary
};
+34
View File
@@ -0,0 +1,34 @@
var Parse = require('../parse');
var Output = require('../output');
var timing = require('../utils/timing');
var options = require('./options');
var getBook = require('./getBook');
var getOutputFolder = require('./getOutputFolder');
module.exports = {
name: 'build [book] [output]',
description: 'build a book',
options: [
options.log,
options.format,
options.timing
],
exec: function(args, kwargs) {
var book = getBook(args, kwargs);
var outputFolder = getOutputFolder(args);
var Generator = Output.getGenerator(kwargs.format);
return Parse.parseBook(book)
.then(function(resultBook) {
return Output.generate(Generator, resultBook, {
root: outputFolder
});
})
.fin(function() {
if (kwargs.timing) timing.dump(book.getLogger());
});
}
};
+76
View File
@@ -0,0 +1,76 @@
var path = require('path');
var tmp = require('tmp');
var Promise = require('../utils/promise');
var fs = require('../utils/fs');
var Parse = require('../parse');
var Output = require('../output');
var options = require('./options');
var getBook = require('./getBook');
module.exports = function(format) {
return {
name: (format + ' [book] [output]'),
description: 'build a book into an ebook file',
options: [
options.log
],
exec: function(args, kwargs) {
// Output file will be stored in
var outputFile = args[1] || ('book.' + format);
// Create temporary directory
var outputFolder = tmp.dirSync().name;
var book = getBook(args, kwargs);
var logger = book.getLogger();
var Generator = Output.getGenerator('ebook');
return Parse.parseBook(book)
.then(function(resultBook) {
return Output.generate(Generator, resultBook, {
root: outputFolder,
format: format
});
})
// Extract ebook file
.then(function(output) {
var book = output.getBook();
var languages = book.getLanguages();
if (book.isMultilingual()) {
return Promise.ForEach(languages, function(lang) {
var langID = lang.getID();
var langOutputFile = path.join(
path.dirname(outputFile),
path.basename(outputFile, format) + '_' + langID + '.' + format
);
return fs.copy(
path.resolve(outputFolder, langID, 'index.' + format),
langOutputFile
);
})
.thenResolve(languages.getCount());
} else {
return fs.copy(
path.resolve(outputFolder, 'index.' + format),
outputFile
).thenResolve(1);
}
})
// Log end
.then(function(count) {
logger.info.ok(count + ' file(s) generated');
logger.debug('cleaning up... ');
return logger.debug.promise(fs.rmDir(outputFolder));
});
}
};
};
+23
View File
@@ -0,0 +1,23 @@
var path = require('path');
var Book = require('../models/book');
var createNodeFS = require('../fs/node');
/**
Return a book instance to work on from
command line args/kwargs
@param {Array} args
@param {Object} kwargs
@return {Book}
*/
function getBook(args, kwargs) {
var input = path.resolve(args[0] || process.cwd());
var logLevel = kwargs.log;
var fs = createNodeFS(input);
var book = Book.createForFS(fs);
return book.setLogLevel(logLevel);
}
module.exports = getBook;
+17
View File
@@ -0,0 +1,17 @@
var path = require('path');
/**
Return path to output folder
@param {Array} args
@return {String}
*/
function getOutputFolder(args) {
var bookRoot = path.resolve(args[0] || process.cwd());
var defaultOutputRoot = path.join(bookRoot, '_book');
var outputFolder = args[1]? path.resolve(process.cwd(), args[1]) : defaultOutputRoot;
return outputFolder;
}
module.exports = getOutputFolder;
-140
View File
@@ -1,140 +0,0 @@
var _ = require('lodash');
var path = require('path');
var Book = require('../book');
var NodeFS = require('../fs/node');
var Logger = require('../utils/logger');
var Promise = require('../utils/promise');
var fs = require('../utils/fs');
var JSONOutput = require('../output/json');
var WebsiteOutput = require('../output/website');
var EBookOutput = require('../output/ebook');
var nodeFS = new NodeFS();
var LOG_OPTION = {
name: 'log',
description: 'Minimum log level to display',
values: _.chain(Logger.LEVELS)
.keys()
.map(function(s) {
return s.toLowerCase();
})
.value(),
defaults: 'info'
};
var FORMAT_OPTION = {
name: 'format',
description: 'Format to build to',
values: ['website', 'json', 'ebook'],
defaults: 'website'
};
var FORMATS = {
json: JSONOutput,
website: WebsiteOutput,
ebook: EBookOutput
};
// Commands which is processing a book
// the root of the book is the first argument (or current directory)
function bookCmd(fn) {
return function(args, kwargs) {
var input = path.resolve(args[0] || process.cwd());
var book = new Book({
fs: nodeFS,
root: input,
logLevel: kwargs.log
});
return fn(book, args.slice(1), kwargs);
};
}
// Commands which is working on a Output instance
function outputCmd(fn) {
return bookCmd(function(book, args, kwargs) {
var Out = FORMATS[kwargs.format];
var outputFolder = undefined;
// Set output folder
if (args[0]) {
outputFolder = path.resolve(process.cwd(), args[0]);
}
return fn(new Out(book, {
root: outputFolder
}), args);
});
}
// Command to generate an ebook
function ebookCmd(format) {
return {
name: format + ' [book] [output] [file]',
description: 'generates ebook '+format,
options: [
LOG_OPTION
],
exec: bookCmd(function(book, args, kwargs) {
return fs.tmpDir()
.then(function(dir) {
var ext = '.'+format;
var outputFile = path.resolve(process.cwd(), args[0] || ('book' + ext));
var output = new EBookOutput(book, {
root: dir,
format: format
});
return output.book.parse()
.then(function() {
return output.generate();
})
// Copy the ebook files
.then(function() {
if (output.book.isMultilingual()) {
return Promise.serie(output.book.langs.list(), function(lang) {
var _outputFile = path.join(
path.dirname(outputFile),
path.basename(outputFile, ext) + '_' + lang.id + ext
);
return fs.copy(
path.resolve(dir, lang.id, 'index' + ext),
_outputFile
);
})
.thenResolve(output.book.langs.count());
} else {
return fs.copy(
path.resolve(dir, 'index' + ext),
outputFile
).thenResolve(1);
}
})
.then(function(n) {
output.book.log.info.ok(n+' file(s) generated');
output.book.log.info('cleaning up... ');
return output.book.log.info.promise(fs.rmDir(dir));
});
});
})
};
}
module.exports = {
nodeFS: nodeFS,
bookCmd: bookCmd,
outputCmd: outputCmd,
ebookCmd: ebookCmd,
options: {
log: LOG_OPTION,
format: FORMAT_OPTION
},
FORMATS: FORMATS
};
+11 -198
View File
@@ -1,199 +1,12 @@
/* eslint-disable no-console */
var buildEbook = require('./buildEbook');
var _ = require('lodash');
var path = require('path');
var tinylr = require('tiny-lr');
var Promise = require('../utils/promise');
var PluginsManager = require('../plugins');
var Book = require('../book');
var helper = require('./helper');
var Server = require('./server');
var watch = require('./watch');
module.exports = {
commands: [
{
name: 'init [book]',
description: 'setup and create files for chapters',
options: [
helper.options.log
],
exec: function(args) {
var input = path.resolve(args[0] || process.cwd());
return Book.init(helper.nodeFS, input);
}
},
{
name: 'parse [book]',
description: 'parse and returns debug information for a book',
options: [
helper.options.log
],
exec: helper.bookCmd(function(book) {
return book.parse()
.then(function() {
book.log.info.ln('Book located in:', book.root);
book.log.info.ln('');
if (book.config.exists()) book.log.info.ln('Configuration:', book.config.path);
if (book.isMultilingual()) {
book.log.info.ln('Multilingual book detected:', book.langs.path);
} else {
book.log.info.ln('Readme:', book.readme.path);
book.log.info.ln('Summary:', book.summary.path);
if (book.glossary.exists()) book.log.info.ln('Glossary:', book.glossary.path);
book.log.info.ln('Pages:');
_.each(book.pages, function(page) {
book.log.info.ln('\t-', page.path);
});
}
});
})
},
{
name: 'install [book]',
description: 'install all plugins dependencies',
options: [
helper.options.log
],
exec: helper.bookCmd(function(book, args) {
var plugins = new PluginsManager(book);
return book.config.load()
.then(function() {
return plugins.install();
});
})
},
{
name: 'build [book] [output]',
description: 'build a book',
options: [
helper.options.log,
helper.options.format
],
exec: helper.outputCmd(function(output, args, kwargs) {
return output.book.parse()
.then(function() {
return output.generate();
});
})
},
helper.ebookCmd('pdf'),
helper.ebookCmd('epub'),
helper.ebookCmd('mobi'),
{
name: 'serve [book]',
description: 'Build then serve a book from a directory',
options: [
{
name: 'port',
description: 'Port for server to listen on',
defaults: 4000
},
{
name: 'lrport',
description: 'Port for livereload server to listen on',
defaults: 35729
},
{
name: 'watch',
description: 'Enable/disable file watcher',
defaults: true
},
helper.options.format,
helper.options.log
],
exec: function(args, kwargs) {
var input = path.resolve(args[0] || process.cwd());
var server = new Server();
// Init livereload server
var lrServer = tinylr({});
var port = kwargs.port;
var lrPath;
var generate = function() {
// Stop server if running
if (server.isRunning()) console.log('Stopping server');
return server.stop()
// Generate the book
.then(function() {
var book = new Book({
fs: helper.nodeFS,
root: input,
logLevel: kwargs.log
});
return book.parse()
.then(function() {
// Add livereload plugin
book.config.set('plugins',
book.config.get('plugins')
.concat([
{ name: 'livereload' }
])
);
var Out = helper.FORMATS[kwargs.format];
var output = new Out(book);
return output.generate()
.thenResolve(output);
});
})
// Start server and watch changes
.then(function(output) {
console.log();
console.log('Starting server ...');
return server.start(output.root(), port)
.then(function() {
console.log('Serving book on http://localhost:'+port);
if (lrPath) {
// trigger livereload
lrServer.changed({
body: {
files: [lrPath]
}
});
}
if (!kwargs.watch) return;
return watch(output.book.root)
.then(function(filepath) {
// set livereload path
lrPath = filepath;
console.log('Restart after change in file', filepath);
console.log('');
return generate();
});
});
});
};
return Promise.nfcall(lrServer.listen.bind(lrServer), kwargs.lrport)
.then(function() {
console.log('Live reload server started on port:', kwargs.lrport);
console.log('Press CTRL+C to quit ...');
console.log('');
return generate();
});
}
}
]
};
module.exports = [
require('./build'),
require('./serve'),
require('./install'),
require('./parse'),
require('./init'),
buildEbook('pdf'),
buildEbook('epub'),
buildEbook('mobi')
];
+17
View File
@@ -0,0 +1,17 @@
var path = require('path');
var options = require('./options');
var initBook = require('../init');
module.exports = {
name: 'install [book]',
description: 'setup and create files for chapters',
options: [
options.log
],
exec: function(args, kwargs) {
var bookRoot = path.resolve(process.cwd(), args[0] || './');
return initBook(bookRoot);
}
};
+21
View File
@@ -0,0 +1,21 @@
var options = require('./options');
var getBook = require('./getBook');
var Parse = require('../parse');
var Plugins = require('../plugins');
module.exports = {
name: 'install [book]',
description: 'install all plugins dependencies',
options: [
options.log
],
exec: function(args, kwargs) {
var book = getBook(args, kwargs);
return Parse.parseConfig(book)
.then(function(resultBook) {
return Plugins.installPlugins(resultBook);
});
}
};
+30
View File
@@ -0,0 +1,30 @@
var Logger = require('../utils/logger');
var logOptions = {
name: 'log',
description: 'Minimum log level to display',
values: Object.keys(Logger.LEVELS)
.map(function(s) {
return s.toLowerCase();
}),
defaults: 'info'
};
var formatOption = {
name: 'format',
description: 'Format to build to',
values: ['website', 'json', 'ebook'],
defaults: 'website'
};
var timingOption = {
name: 'timing',
description: 'Print timing debug information',
defaults: false
};
module.exports = {
log: logOptions,
format: formatOption,
timing: timingOption
};
+79
View File
@@ -0,0 +1,79 @@
var options = require('./options');
var getBook = require('./getBook');
var Parse = require('../parse');
function printBook(book) {
var logger = book.getLogger();
var config = book.getConfig();
var configFile = config.getFile();
var summary = book.getSummary();
var summaryFile = summary.getFile();
var readme = book.getReadme();
var readmeFile = readme.getFile();
var glossary = book.getGlossary();
var glossaryFile = glossary.getFile();
if (configFile.exists()) {
logger.info.ln('Configuration file is', configFile.getPath());
}
if (readmeFile.exists()) {
logger.info.ln('Introduction file is', readmeFile.getPath());
}
if (glossaryFile.exists()) {
logger.info.ln('Glossary file is', glossaryFile.getPath());
}
if (summaryFile.exists()) {
logger.info.ln('Table of Contents file is', summaryFile.getPath());
}
}
function printMultingualBook(book) {
var logger = book.getLogger();
var languages = book.getLanguages();
var books = book.getBooks();
logger.info.ln(languages.size + ' languages');
languages.forEach(function(lang) {
logger.info.ln('Language:', lang.getTitle());
printBook(books.get(lang.getID()));
logger.info.ln('');
});
}
module.exports = {
name: 'parse [book]',
description: 'parse and print debug information about a book',
options: [
options.log
],
exec: function(args, kwargs) {
var book = getBook(args, kwargs);
var logger = book.getLogger();
return Parse.parseBook(book)
.then(function(resultBook) {
var rootFolder = book.getRoot();
var contentFolder = book.getContentRoot();
logger.info.ln('Book located in:', rootFolder);
if (contentFolder != rootFolder) {
logger.info.ln('Content located in:', contentFolder);
}
if (resultBook.isMultilingual()) {
printMultingualBook(resultBook);
} else {
printBook(resultBook);
}
});
}
};
+93
View File
@@ -0,0 +1,93 @@
/* eslint-disable no-console */
var tinylr = require('tiny-lr');
var Parse = require('../parse');
var Output = require('../output');
var options = require('./options');
var getBook = require('./getBook');
var getOutputFolder = require('./getOutputFolder');
var Server = require('./server');
var watch = require('./watch');
var server, lrServer, lrPath;
function generateBook(args, kwargs) {
var port = kwargs.port;
var outputFolder = getOutputFolder(args);
var book = getBook(args, kwargs);
var Generator = Output.getGenerator(kwargs.format);
// Stop server if running
if (server.isRunning()) console.log('Stopping server');
return server.stop()
.then(function() {
return Parse.parseBook(book)
.then(function(resultBook) {
return Output.generate(Generator, resultBook, {
root: outputFolder
});
});
})
.then(function() {
console.log();
console.log('Starting server ...');
return server.start(outputFolder, port);
})
.then(function() {
console.log('Serving book on http://localhost:'+port);
if (lrPath) {
// trigger livereload
lrServer.changed({
body: {
files: [lrPath]
}
});
}
})
.then(function() {
if (!kwargs.watch) return;
return watch(book.getRoot())
.then(function(filepath) {
// set livereload path
lrPath = filepath;
console.log('Restart after change in file', filepath);
console.log('');
return generateBook(args, kwargs);
});
});
}
module.exports = {
name: 'serve [book] [output]',
description: 'serve the book as a website for testing',
options: [
{
name: 'port',
description: 'Port for server to listen on',
defaults: 4000
},
{
name: 'lrport',
description: 'Port for livereload server to listen on',
defaults: 35729
},
{
name: 'watch',
description: 'Enable/disable file watcher',
defaults: true
},
options.log,
options.format
],
exec: function(args, kwargs) {
server = new Server();
lrServer = tinylr({});
return generateBook(args, kwargs);
}
};
+17 -4
View File
@@ -6,20 +6,28 @@ var url = require('url');
var Promise = require('../utils/promise');
var Server = function() {
function Server() {
this.running = null;
this.dir = null;
this.port = 0;
this.sockets = [];
};
}
util.inherits(Server, events.EventEmitter);
// Return true if the server is running
/**
Return true if the server is running
@return {Boolean}
*/
Server.prototype.isRunning = function() {
return !!this.running;
};
// Stop the server
/**
Stop the server
@return {Promise}
*/
Server.prototype.stop = function() {
var that = this;
if (!this.isRunning()) return Promise();
@@ -40,6 +48,11 @@ Server.prototype.stop = function() {
return d.promise;
};
/**
Start the server
@return {Promise}
*/
Server.prototype.start = function(dir, port) {
var that = this, pre = Promise();
port = port || 8004;
+6 -1
View File
@@ -5,7 +5,12 @@ var chokidar = require('chokidar');
var Promise = require('../utils/promise');
var parsers = require('../parsers');
// Watch a folder and resolve promise once a file is modified
/**
Watch a folder and resolve promise once a file is modified
@param {String} dir
@return {Promise}
*/
function watch(dir) {
var d = Promise.defer();
dir = path.resolve(dir);
-137
View File
@@ -1,137 +0,0 @@
var _ = require('lodash');
var semver = require('semver');
var gitbook = require('../gitbook');
var Promise = require('../utils/promise');
var error = require('../utils/error');
var validator = require('./validator');
var plugins = require('./plugins');
// Config files to tested (sorted)
var CONFIG_FILES = [
'book.js',
'book.json'
];
/*
Config is an interface for the book's configuration stored in "book.json" (or "book.js")
*/
function Config(book, baseConfig) {
this.book = book;
this.fs = book.fs;
this.log = book.log;
this.path = '';
this.baseConfig = baseConfig || {};
this.replace({});
}
// Load configuration of the book
// and verify that the configuration is satisfying
Config.prototype.load = function() {
var that = this;
var isLanguageBook = this.book.isLanguageBook();
// Try all potential configuration file
return Promise.some(CONFIG_FILES, function(filename) {
that.log.debug.ln('try loading configuration from', filename);
return that.fs.loadAsObject(that.book.resolve(filename))
.then(function(_config) {
that.log.debug.ln('configuration loaded from', filename);
that.path = filename;
return that.replace(_config);
})
.fail(function(err) {
if (err.code != 'MODULE_NOT_FOUND') throw(err);
else return Promise(false);
});
})
.then(function() {
if (!isLanguageBook) {
if (!gitbook.satisfies(that.options.gitbook)) {
throw new Error('GitBook version doesn\'t satisfy version required by the book: '+that.options.gitbook);
}
if (that.options.gitbook != '*' && !semver.satisfies(semver.inc(gitbook.version, 'patch'), that.options.gitbook)) {
that.log.warn.ln('gitbook version specified in your book.json might be too strict for future patches, \'>='+(_.first(gitbook.version.split('.'))+'.x.x')+'\' is more adequate');
}
that.options.plugins = plugins.toList(that.options.plugins);
} else {
// Multilingual book should inherits the plugins list from parent
that.options.plugins = that.book.parent.config.get('plugins');
}
that.options.gitbook = gitbook.version;
});
};
// Replace the whole configuration
Config.prototype.replace = function(options) {
var that = this;
// Extend base config
options = _.defaults(_.cloneDeep(options), this.baseConfig);
// Validate the config
this.options = validator.validate(options);
// options.input == book.root
Object.defineProperty(this.options, 'input', {
get: function () {
return that.book.root;
}
});
// options.originalInput == book.parent.root
Object.defineProperty(this.options, 'originalInput', {
get: function () {
return that.book.parent? that.book.parent.root : undefined;
}
});
error.deprecateField(this.options, 'generator', (this.book.output? this.book.output.name : null), '"options.generator" property is deprecated, use "output.name" instead');
error.deprecateField(this.options, 'output', (this.book.output && this.book.output.root? this.book.output.root() : null), '"options.output" property is deprecated, use "output.root()" instead');
};
// Return true if book has a configuration file
Config.prototype.exists = function() {
return Boolean(this.path);
};
// Return path to a structure file
// Strip the extension by default
Config.prototype.getStructure = function(name, dontStripExt) {
var filename = this.options.structure[name];
if (dontStripExt) return filename;
filename = filename.split('.').slice(0, -1).join('.');
return filename;
};
// Return a configuration using a key and a default value
Config.prototype.get = function(key, def) {
return _.get(this.options, key, def);
};
// Update a configuration
Config.prototype.set = function(key, value) {
return _.set(this.options, key, value);
};
// Return a dump of the configuration
Config.prototype.dump = function() {
var opts = _.omit(this.options, 'generator', 'output');
return _.cloneDeep(opts);
};
// Return templating context
Config.prototype.getContext = function() {
return {
config: this.book.config.dump()
};
};
module.exports = Config;
-67
View File
@@ -1,67 +0,0 @@
var _ = require('lodash');
// Default plugins added to each books
var DEFAULT_PLUGINS = ['highlight', 'search', 'lunr', 'sharing', 'fontsettings', 'theme-default'];
// Return true if a plugin is a default plugin
function isDefaultPlugin(name, version) {
return _.contains(DEFAULT_PLUGINS, name);
}
// Normalize a list of plugins to use
function normalizePluginsList(plugins) {
// Normalize list to an array
plugins = _.isString(plugins) ? plugins.split(',') : (plugins || []);
// Remove empty parts
plugins = _.compact(plugins);
// Divide as {name, version} to handle format like 'myplugin@1.0.0'
plugins = _.map(plugins, function(plugin) {
if (plugin.name) return plugin;
var parts = plugin.split('@');
var name = parts[0];
var version = parts.slice(1).join('@');
return {
'name': name,
'version': version // optional
};
});
// List plugins to remove
var toremove = _.chain(plugins)
.filter(function(plugin) {
return plugin.name.length > 0 && plugin.name[0] == '-';
})
.map(function(plugin) {
return plugin.name.slice(1);
})
.value();
// Merge with defaults
_.each(DEFAULT_PLUGINS, function(plugin) {
if (_.find(plugins, { name: plugin })) {
return;
}
plugins.push({
'name': plugin
});
});
// Remove plugin that start with '-'
plugins = _.filter(plugins, function(plugin) {
return !_.contains(toremove, plugin.name) && !(plugin.name.length > 0 && plugin.name[0] == '-');
});
// Remove duplicates
plugins = _.uniq(plugins, 'name');
return plugins;
}
module.exports = {
isDefaultPlugin: isDefaultPlugin,
toList: normalizePluginsList
};
+6
View File
@@ -0,0 +1,6 @@
var Immutable = require('immutable');
var jsonSchemaDefaults = require('json-schema-defaults');
var schema = require('./configSchema');
module.exports = Immutable.fromJS(jsonSchemaDefaults(schema));
+5
View File
@@ -0,0 +1,5 @@
// Configuration files to test (sorted)
module.exports = [
'book.js',
'book.json'
];
+51
View File
@@ -0,0 +1,51 @@
var Immutable = require('immutable');
var TemplateBlock = require('../models/templateBlock');
module.exports = Immutable.Map({
html: TemplateBlock({
name: 'html',
process: function(blk) {
return blk;
}
}),
code: TemplateBlock({
name: 'code',
process: function(blk) {
return {
html: false,
body: blk.body
};
}
}),
markdown: TemplateBlock({
name: 'markdown',
process: function(blk) {
return this.book.renderInline('markdown', blk.body)
.then(function(out) {
return { body: out };
});
}
}),
asciidoc: TemplateBlock({
name: 'asciidoc',
process: function(blk) {
return this.book.renderInline('asciidoc', blk.body)
.then(function(out) {
return { body: out };
});
}
}),
markup: TemplateBlock({
name: 'markup',
process: function(blk) {
return this.book.renderInline(this.ctx.file.type, blk.body)
.then(function(out) {
return { body: out };
});
}
})
});
@@ -1,7 +1,7 @@
var Immutable = require('immutable');
var moment = require('moment');
module.exports = {
module.exports = Immutable.Map({
// Format a date
// ex: 'MMMM Do YYYY, h:mm:ss a
date: function(time, format) {
@@ -12,4 +12,4 @@ module.exports = {
dateFromNow: function(time) {
return moment(time).fromNow();
}
};
});
+14
View File
@@ -0,0 +1,14 @@
var Immutable = require('immutable');
/*
List of default plugins for all books,
default plugins should be installed in node dependencies of GitBook
*/
module.exports = Immutable.List([
'highlight',
'search',
'lunr',
'sharing',
'fontsettings',
'theme-default'
]);
+6
View File
@@ -0,0 +1,6 @@
// Files containing ignore pattner (sorted by priority)
module.exports = [
'.ignore',
'.gitignore',
'.bookignore'
];
+2
View File
@@ -0,0 +1,2 @@
module.exports = '_assets';
+8
View File
@@ -0,0 +1,8 @@
module.exports = [
'init',
'finish',
'finish:before',
'config',
'page',
'page:before'
];
+5
View File
@@ -0,0 +1,5 @@
/*
All GitBook plugins are NPM packages starting with this prefix.
*/
module.exports = 'gitbook-plugin-';
+6
View File
@@ -0,0 +1,6 @@
var Immutable = require('immutable');
module.exports = Immutable.List([
'js',
'css'
]);
+2
View File
@@ -0,0 +1,2 @@
module.exports = '_layouts';
+83
View File
@@ -0,0 +1,83 @@
jest.autoMockOff();
describe('MockFS', function() {
var createMockFS = require('../mock');
var fs = createMockFS({
'README.md': 'Hello World',
'SUMMARY.md': '# Summary',
'folder': {
'test.md': 'Cool',
'folder2': {
'hello.md': 'Hello',
'world.md': 'World'
}
}
});
describe('exists', function() {
pit('must return true for a file', function() {
return fs.exists('README.md')
.then(function(result) {
expect(result).toBeTruthy();
});
});
pit('must return false for a non existing file', function() {
return fs.exists('README_NOTEXISTS.md')
.then(function(result) {
expect(result).toBeFalsy();
});
});
pit('must return true for a directory', function() {
return fs.exists('folder')
.then(function(result) {
expect(result).toBeTruthy();
});
});
pit('must return true for a deep file', function() {
return fs.exists('folder/test.md')
.then(function(result) {
expect(result).toBeTruthy();
});
});
pit('must return true for a deep file (2)', function() {
return fs.exists('folder/folder2/hello.md')
.then(function(result) {
expect(result).toBeTruthy();
});
});
});
describe('readAsString', function() {
pit('must return content for a file', function() {
return fs.readAsString('README.md')
.then(function(result) {
expect(result).toBe('Hello World');
});
});
pit('must return content for a deep file', function() {
return fs.readAsString('folder/test.md')
.then(function(result) {
expect(result).toBe('Cool');
});
});
});
describe('readDir', function() {
pit('must return content for a directory', function() {
return fs.readDir('./')
.then(function(files) {
expect(files.size).toBe(3);
expect(files.includes('README.md')).toBeTruthy();
expect(files.includes('SUMMARY.md')).toBeTruthy();
expect(files.includes('folder/')).toBeTruthy();
});
});
});
});
-106
View File
@@ -1,106 +0,0 @@
var _ = require('lodash');
var path = require('path');
var Promise = require('../utils/promise');
/*
A filesystem is an interface to read files
GitBook can works with a virtual filesystem, for example in the browser.
*/
// .readdir return files/folder as a list of string, folder ending with '/'
function pathIsFolder(filename) {
return _.last(filename) == '/' || _.last(filename) == '\\';
}
function FS() {
}
// Check if a file exists, run a Promise(true) if that's the case, Promise(false) otherwise
FS.prototype.exists = function(filename) {
// To implement for each fs
};
// Read a file and returns a promise with the content as a buffer
FS.prototype.read = function(filename) {
// To implement for each fs
};
// Read stat infos about a file
FS.prototype.stat = function(filename) {
// To implement for each fs
};
// List files/directories in a directory
FS.prototype.readdir = function(folder) {
// To implement for each fs
};
// These methods don't require to be redefined, by default it uses .exists, .read, .write, .list
// For optmization, it can be redefined:
// List files in a directory
FS.prototype.listFiles = function(folder) {
return this.readdir(folder)
.then(function(files) {
return _.reject(files, pathIsFolder);
});
};
// List all files in the fs
FS.prototype.listAllFiles = function(folder) {
var that = this;
return this.readdir(folder)
.then(function(files) {
return _.reduce(files, function(prev, file) {
return prev.then(function(output) {
var isDirectory = pathIsFolder(file);
if (!isDirectory) {
output.push(file);
return output;
} else {
return that.listAllFiles(path.join(folder, file))
.then(function(files) {
return output.concat(_.map(files, function(_file) {
return path.join(file, _file);
}));
});
}
});
}, Promise([]));
});
};
// Read a file as a string (utf-8)
FS.prototype.readAsString = function(filename) {
return this.read(filename)
.then(function(buf) {
return buf.toString('utf-8');
});
};
// Find a file in a folder (case incensitive)
// Return the real filename
FS.prototype.findFile = function findFile(root, filename) {
return this.listFiles(root)
.then(function(files) {
return _.find(files, function(file) {
return (file.toLowerCase() == filename.toLowerCase());
});
});
};
// Load a JSON file
// By default, fs only supports JSON
FS.prototype.loadAsObject = function(filename) {
return this.readAsString(filename)
.then(function(str) {
return JSON.parse(str);
});
};
module.exports = FS;
+95
View File
@@ -0,0 +1,95 @@
var path = require('path');
var is = require('is');
var Buffer = require('buffer').Buffer;
var Immutable = require('immutable');
var FS = require('../models/fs');
var error = require('../utils/error');
/**
Create a fake filesystem for unit testing GitBook.
@param {Map<String:String|Map>}
*/
function createMockFS(files) {
files = Immutable.fromJS(files);
var mtime = new Date();
function getFile(filePath) {
var parts = path.normalize(filePath).split('/');
return parts.reduce(function(list, part, i) {
if (!list) return null;
var file;
if (!part || part === '.') file = list;
else file = list.get(part);
if (!file) return null;
if (is.string(file)) {
if (i === (parts.length - 1)) return file;
else return null;
}
return file;
}, files);
}
function fsExists(filePath) {
return Boolean(getFile(filePath) !== null);
}
function fsReadFile(filePath) {
var file = getFile(filePath);
if (!is.string(file)) {
throw error.FileNotFoundError({
filename: filePath
});
}
return new Buffer(file, 'utf8');
}
function fsStatFile(filePath) {
var file = getFile(filePath);
if (!file) {
throw error.FileNotFoundError({
filename: filePath
});
}
return {
mtime: mtime
};
}
function fsReadDir(filePath) {
var dir = getFile(filePath);
if (!dir || is.string(dir)) {
throw error.FileNotFoundError({
filename: filePath
});
}
return dir
.map(function(content, name) {
if (!is.string(content)) {
name = name + '/';
}
return name;
})
.valueSeq();
}
return FS.create({
root: '',
fsExists: fsExists,
fsReadFile: fsReadFile,
fsStatFile: fsStatFile,
fsReadDir: fsReadDir
});
}
module.exports = createMockFS;
+25 -51
View File
@@ -1,36 +1,15 @@
var _ = require('lodash');
var util = require('util');
var path = require('path');
var Immutable = require('immutable');
var fs = require('../utils/fs');
var Promise = require('../utils/promise');
var BaseFS = require('./');
var FS = require('../models/fs');
function NodeFS() {
BaseFS.call(this);
}
util.inherits(NodeFS, BaseFS);
// Check if a file exists, run a Promise(true) if that's the case, Promise(false) otherwise
NodeFS.prototype.exists = function(filename) {
return fs.exists(filename);
};
// Read a file and returns a promise with the content as a buffer
NodeFS.prototype.read = function(filename) {
return fs.readFile(filename);
};
// Read stat infos about a file
NodeFS.prototype.stat = function(filename) {
return fs.stat(filename);
};
// List files in a directory
NodeFS.prototype.readdir = function(folder) {
function fsReadDir(folder) {
return fs.readdir(folder)
.then(function(files) {
return _.chain(files)
files = Immutable.List(files);
return files
.map(function(file) {
if (file == '.' || file == '..') return;
@@ -38,29 +17,24 @@ NodeFS.prototype.readdir = function(folder) {
if (stat.isDirectory()) file = file + path.sep;
return file;
})
.compact()
.value();
.filter(function(file) {
return Boolean(file);
});
});
}
function fsLoadObject(filename) {
return require(filename);
}
module.exports = function createNodeFS(root) {
return FS.create({
root: root,
fsExists: fs.exists,
fsReadFile: fs.readFile,
fsStatFile: fs.stat,
fsReadDir: fsReadDir,
fsLoadObject: fsLoadObject
});
};
// Load a JSON/JS file
NodeFS.prototype.loadAsObject = function(filename) {
return Promise()
.then(function() {
var jsFile;
try {
jsFile = require.resolve(filename);
// Invalidate node.js cache for livreloading
delete require.cache[jsFile];
return require(jsFile);
}
catch(err) {
return Promise.reject(err);
}
});
};
module.exports = NodeFS;
+8 -13
View File
@@ -6,8 +6,13 @@ var VERSION_STABLE = VERSION.replace(/\-(\S+)/g, '');
var START_TIME = new Date();
// Verify that this gitbook version satisfies a requirement
// We can't directly use samver.satisfies since it will break all plugins when gitbook version is a prerelease (beta, alpha)
/**
Verify that this gitbook version satisfies a requirement
We can't directly use samver.satisfies since it will break all plugins when gitbook version is a prerelease (beta, alpha)
@param {String} condition
@return {Boolean}
*/
function satisfies(condition) {
// Test with real version
if (semver.satisfies(VERSION, condition)) return true;
@@ -16,18 +21,8 @@ function satisfies(condition) {
return semver.satisfies(VERSION_STABLE, condition);
}
// Return templating/json context for gitbook itself
function getContext() {
return {
gitbook: {
version: pkg.version,
time: START_TIME
}
};
}
module.exports = {
version: pkg.version,
satisfies: satisfies,
getContext: getContext
START_TIME: START_TIME
};
+9 -6
View File
@@ -1,7 +1,10 @@
var Book = require('./book');
var cli = require('./cli');
var extend = require('extend');
module.exports = {
Book: Book,
commands: cli.commands
};
var common = require('./browser');
module.exports = extend({
initBook: require('./init'),
createNodeFS: require('./fs/node'),
Output: require('./output'),
commands: require('./cli')
}, common);
+65 -52
View File
@@ -1,66 +1,79 @@
var path = require('path');
var createNodeFS = require('./fs/node');
var fs = require('./utils/fs');
var Promise = require('./utils/promise');
var File = require('./models/file');
var Readme = require('./models/readme');
var Book = require('./models/book');
var Parse = require('./parse');
// Initialize folder structure for a book
// Read SUMMARY to created the right chapter
function initBook(book) {
var extensionToUse = '.md';
/**
Initialize folder structure for a book
Read SUMMARY to created the right chapter
book.log.info.ln('init book at', book.root);
return fs.mkdirp(book.root)
@param {Book}
@param {String}
@return {Promise}
*/
function initBook(rootFolder) {
var extension = '.md';
return fs.mkdirp(rootFolder)
// Parse the summary and readme
.then(function() {
return book.config.load();
})
.then(function() {
book.log.info.ln('detect structure from SUMMARY (if it exists)');
return book.summary.load();
})
.then(function() {
var summary = book.summary.path || 'SUMMARY.md';
var articles = book.summary.flatten();
var fs = createNodeFS(rootFolder);
var book = Book.createForFS(fs);
// Use extension of summary
extensionToUse = path.extname(summary);
return Parse.parseReadme(book)
// Readme doesn't have a path
if (!articles[0].path) {
articles[0].path = 'README' + extensionToUse;
}
// Summary doesn't exists? create one
if (!book.summary.path) {
articles.push({
title: 'Summary',
path: 'SUMMARY'+extensionToUse
});
}
// Create files that don't exist
return Promise.serie(articles, function(article) {
if (!article.path) return;
var absolutePath = book.resolve(article.path);
return fs.exists(absolutePath)
.then(function(exists) {
if(exists) {
book.log.info.ln('found', article.path);
return;
} else {
book.log.info.ln('create', article.path);
}
return fs.mkdirp(path.dirname(absolutePath))
.then(function() {
return fs.writeFile(absolutePath, '# '+article.title+'\n\n');
});
});
// Setup default readme if doesn't found one
.fail(function() {
var readmeFile = File.createWithFilepath('README' + extension);
var readme = Readme.create(readmeFile);
return book.setReadme(readme);
});
})
.then(function() {
book.log.info.ln('initialization is finished');
.then(Parse.parseSummary)
.then(function(book) {
var logger = book.getLogger();
var summary = book.getSummary();
var summaryFile = summary.getFile();
var summaryFilename = summaryFile.getPath() || ('SUMMARY' + extension);
var articles = summary.getArticlesAsList();
// Write pages
return Promise.forEach(articles, function(article) {
var filePath = path.join(rootFolder, article.getPath());
if (!filePath) return;
return fs.assertFile(filePath, function() {
return fs.ensureFile(filePath)
.then(function() {
logger.info.ln('create', article.getPath());
return fs.writeFile(filePath, '# ' + article.getTitle() + '\n\n');
});
});
})
// Write summary
.then(function() {
var filePath = path.join(rootFolder, summaryFilename);
return fs.ensureFile(filePath)
.then(function() {
logger.info.ln('create ' + path.basename(filePath));
return fs.writeFile(filePath, summary.toText(extension));
});
})
// Log end
.then(function() {
logger.info.ln('initialization is finished');
});
});
}
+35
View File
@@ -0,0 +1,35 @@
var extend = require('extend');
var gitbook = require('../gitbook');
var encodeSummary = require('./encodeSummary');
var encodeGlossary = require('./encodeGlossary');
var encodeReadme = require('./encodeReadme');
/**
Encode a book to JSON
@param {Book}
@return {Object}
*/
function encodeBookToJson(book) {
var config = book.getConfig();
var language = book.getLanguage();
var variables = config.getValue('variables', {});
return {
summary: encodeSummary(book.getSummary()),
glossary: encodeGlossary(book.getGlossary()),
readme: encodeReadme(book.getReadme()),
config: book.getConfig().getValues().toJS(),
gitbook: {
version: gitbook.version,
time: gitbook.START_TIME
},
book: extend({
language: language? language : undefined
}, variables.toJS())
};
}
module.exports = encodeBookToJson;
+22
View File
@@ -0,0 +1,22 @@
var encodeBook = require('./encodeBook');
var encodePage = require('./encodePage');
var encodeFile = require('./encodeFile');
/**
Return a JSON representation of a book with a specific file
@param {Book} output
@param {Page} page
@return {Object}
*/
function encodeBookWithPage(book, page) {
var file = page.getFile();
var result = encodeBook(book);
result.page = encodePage(page, book.getSummary());
result.file = encodeFile(file);
return result;
}
module.exports = encodeBookWithPage;
+21
View File
@@ -0,0 +1,21 @@
/**
Return a JSON representation of a file
@param {File} file
@return {Object}
*/
function encodeFileToJson(file) {
var filePath = file.getPath();
if (!filePath) {
return undefined;
}
return {
path: filePath,
mtime: file.getMTime(),
type: file.getType()
};
}
module.exports = encodeFileToJson;
+21
View File
@@ -0,0 +1,21 @@
var encodeFile = require('./encodeFile');
var encodeGlossaryEntry = require('./encodeGlossaryEntry');
/**
Encode a glossary to JSON
@param {Glossary}
@return {Object}
*/
function encodeGlossary(glossary) {
var file = glossary.getFile();
var entries = glossary.getEntries();
return {
file: encodeFile(file),
entries: entries
.map(encodeGlossaryEntry).toJS()
};
}
module.exports = encodeGlossary;
+16
View File
@@ -0,0 +1,16 @@
/**
Encode a SummaryArticle to JSON
@param {GlossaryEntry}
@return {Object}
*/
function encodeGlossaryEntry(entry) {
return {
id: entry.getID(),
name: entry.getName(),
description: entry.getDescription()
};
}
module.exports = encodeGlossaryEntry;
+25
View File
@@ -0,0 +1,25 @@
var encodeBook = require('./encodeBook');
/**
Encode an output to JSON
@param {Output}
@return {Object}
*/
function encodeOutputToJson(output) {
var book = output.getBook();
var generator = output.getGenerator();
var options = output.getOptions();
var result = encodeBook(book);
result.output = {
name: generator
};
result.options = options.toJS();
return result;
}
module.exports = encodeOutputToJson;
+39
View File
@@ -0,0 +1,39 @@
var encodeSummaryArticle = require('./encodeSummaryArticle');
/**
Return a JSON representation of a page
@param {Page} page
@param {Summary} summary
@return {Object}
*/
function encodePage(page, summary) {
var file = page.getFile();
var attributes = page.getAttributes();
var article = summary.getByPath(file.getPath());
var result = attributes.toJS();
if (article) {
result.title = article.getTitle();
result.level = article.getLevel();
result.depth = article.getDepth();
var nextArticle = summary.getNextArticle(article);
if (nextArticle) {
result.next = encodeSummaryArticle(nextArticle);
}
var prevArticle = summary.getPrevArticle(article);
if (prevArticle) {
result.previous = encodeSummaryArticle(prevArticle);
}
}
result.content = page.getContent();
result.dir = page.getDir();
return result;
}
module.exports = encodePage;
+17
View File
@@ -0,0 +1,17 @@
var encodeFile = require('./encodeFile');
/**
Encode a readme to JSON
@param {Readme}
@return {Object}
*/
function encodeReadme(readme) {
var file = readme.getFile();
return {
file: encodeFile(file)
};
}
module.exports = encodeReadme;
+20
View File
@@ -0,0 +1,20 @@
var encodeFile = require('./encodeFile');
var encodeSummaryPart = require('./encodeSummaryPart');
/**
Encode a summary to JSON
@param {Summary}
@return {Object}
*/
function encodeSummary(summary) {
var file = summary.getFile();
var parts = summary.getParts();
return {
file: encodeFile(file),
parts: parts.map(encodeSummaryPart).toJS()
};
}
module.exports = encodeSummary;
+27
View File
@@ -0,0 +1,27 @@
/**
Encode a SummaryArticle to JSON
@param {SummaryArticle}
@return {Object}
*/
function encodeSummaryArticle(article, recursive) {
var articles = undefined;
if (recursive !== false) {
articles = article.getArticles()
.map(encodeSummaryArticle)
.toJS();
}
return {
title: article.getTitle(),
level: article.getLevel(),
depth: article.getDepth(),
anchor: article.getAnchor(),
url: article.getUrl(),
path: article.getPath(),
articles: articles
};
}
module.exports = encodeSummaryArticle;
+17
View File
@@ -0,0 +1,17 @@
var encodeSummaryArticle = require('./encodeSummaryArticle');
/**
Encode a SummaryPart to JSON
@param {SummaryPart}
@return {Object}
*/
function encodeSummaryPart(part) {
return {
title: part.getTitle(),
articles: part.getArticles()
.map(encodeSummaryArticle).toJS()
};
}
module.exports = encodeSummaryPart;
+11
View File
@@ -0,0 +1,11 @@
module.exports = {
encodeOutput: require('./encodeOutput'),
encodeBookWithPage: require('./encodeBookWithPage'),
encodeBook: require('./encodeBook'),
encodeFile: require('./encodeFile'),
encodePage: require('./encodePage'),
encodeSummary: require('./encodeSummary'),
encodeSummaryArticle: require('./encodeSummaryArticle'),
encodeReadme: require('./encodeReadme')
};
+63
View File
@@ -0,0 +1,63 @@
jest.autoMockOff();
var Immutable = require('immutable');
describe('Config', function() {
var Config = require('../config');
var config = Config.createWithValues({
hello: {
world: 1,
test: 'Hello',
isFalse: false
}
});
describe('getValue', function() {
it('must return value as immutable', function() {
var value = config.getValue('hello');
expect(Immutable.Map.isMap(value)).toBeTruthy();
});
it('must return deep value', function() {
var value = config.getValue('hello.world');
expect(value).toBe(1);
});
it('must return default value if non existant', function() {
var value = config.getValue('hello.nonExistant', 'defaultValue');
expect(value).toBe('defaultValue');
});
it('must not return default value for falsy values', function() {
var value = config.getValue('hello.isFalse', 'defaultValue');
expect(value).toBe(false);
});
});
describe('setValue', function() {
it('must set value as immutable', function() {
var testConfig = config.setValue('hello', {
'cool': 1
});
var value = testConfig.getValue('hello');
expect(Immutable.Map.isMap(value)).toBeTruthy();
expect(value.size).toBe(1);
expect(value.has('cool')).toBeTruthy();
});
it('must set deep value', function() {
var testConfig = config.setValue('hello.world', 2);
var hello = testConfig.getValue('hello');
var world = testConfig.getValue('hello.world');
expect(Immutable.Map.isMap(hello)).toBeTruthy();
expect(hello.size).toBe(3);
expect(world).toBe(2);
});
});
});
+42
View File
@@ -0,0 +1,42 @@
jest.autoMockOff();
describe('Glossary', function() {
var File = require('../file');
var Glossary = require('../glossary');
var GlossaryEntry = require('../glossaryEntry');
var glossary = Glossary.createFromEntries(File(), [
{
name: 'Hello World',
description: 'Awesome!'
},
{
name: 'JavaScript',
description: 'This is a cool language'
}
]);
describe('createFromEntries', function() {
it('must add all entries', function() {
var entries = glossary.getEntries();
expect(entries.size).toBe(2);
});
it('must add entries as GlossaryEntries', function() {
var entries = glossary.getEntries();
var entry = entries.get('hello-world');
expect(entry instanceof GlossaryEntry).toBeTruthy();
});
});
describe('toText', function() {
pit('return as markdown', function() {
return glossary.toText('.md')
.then(function(text) {
expect(text).toContain('# Glossary');
});
});
});
});
+17
View File
@@ -0,0 +1,17 @@
jest.autoMockOff();
describe('GlossaryEntry', function() {
var GlossaryEntry = require('../glossaryEntry');
describe('getID', function() {
it('must return a normalized ID', function() {
var entry = new GlossaryEntry({
name: 'Hello World'
});
expect(entry.getID()).toBe('hello-world');
});
});
});
+29
View File
@@ -0,0 +1,29 @@
jest.autoMockOff();
describe('Plugin', function() {
var Plugin = require('../plugin');
describe('createFromString', function() {
it('must parse name', function() {
var plugin = Plugin.createFromString('hello');
expect(plugin.getName()).toBe('hello');
expect(plugin.getVersion()).toBe('*');
});
it('must parse version', function() {
var plugin = Plugin.createFromString('hello@1.0.0');
expect(plugin.getName()).toBe('hello');
expect(plugin.getVersion()).toBe('1.0.0');
});
});
describe('isLoaded', function() {
it('must return false for empty plugin', function() {
var plugin = Plugin.createFromString('hello');
expect(plugin.isLoaded()).toBe(false);
});
});
});
+81
View File
@@ -0,0 +1,81 @@
describe('Summary', function() {
var File = require('../file');
var Summary = require('../summary');
var summary = Summary.createFromParts(File(), [
{
articles: [
{
title: 'My First Article',
path: 'README.md'
},
{
title: 'My Second Article',
path: 'article.md'
}
]
},
{
title: 'Test'
}
]);
describe('createFromEntries', function() {
it('must add all parts', function() {
var parts = summary.getParts();
expect(parts.size).toBe(2);
});
});
describe('getByLevel', function() {
it('can return a Part', function() {
var part = summary.getByLevel('1');
expect(part).toBeDefined();
expect(part.getArticles().size).toBe(2);
});
it('can return a Part (2)', function() {
var part = summary.getByLevel('2');
expect(part).toBeDefined();
expect(part.getTitle()).toBe('Test');
expect(part.getArticles().size).toBe(0);
});
it('can return an Article', function() {
var article = summary.getByLevel('1.1');
expect(article).toBeDefined();
expect(article.getTitle()).toBe('My First Article');
});
});
describe('getByPath', function() {
it('return correct article', function() {
var article = summary.getByPath('README.md');
expect(article).toBeDefined();
expect(article.getTitle()).toBe('My First Article');
});
it('return correct article', function() {
var article = summary.getByPath('article.md');
expect(article).toBeDefined();
expect(article.getTitle()).toBe('My Second Article');
});
});
describe('toText', function() {
pit('return as markdown', function() {
return summary.toText('.md')
.then(function(text) {
expect(text).toContain('# Summary');
});
});
});
});
+106
View File
@@ -0,0 +1,106 @@
var nunjucks = require('nunjucks');
var Immutable = require('immutable');
var Promise = require('../../utils/promise');
describe('TemplateBlock', function() {
var TemplateBlock = require('../templateBlock');
describe('create', function() {
pit('must initialize a simple TemplateBlock from a function', function() {
var templateBlock = TemplateBlock.create('sayhello', function(block) {
return '<p>Hello, World!</p>';
});
// Check basic templateBlock properties
expect(templateBlock.getName()).toBe('sayhello');
expect(templateBlock.getPost()).toBeNull();
expect(templateBlock.getParse()).toBeTruthy();
expect(templateBlock.getEndTag()).toBe('endsayhello');
expect(templateBlock.getBlocks().size).toBe(0);
expect(templateBlock.getShortcuts().size).toBe(0);
expect(templateBlock.getExtensionName()).toBe('BlocksayhelloExtension');
// Check result of applying block
return Promise()
.then(function() {
return templateBlock.applyBlock();
})
.then(function(result) {
expect(result.name).toBe('sayhello');
expect(result.body).toBe('<p>Hello, World!</p>');
});
});
});
describe('toNunjucksExt()', function() {
pit('must create a valid nunjucks extension', function() {
var templateBlock = TemplateBlock.create('sayhello', function(block) {
return '<p>Hello, World!</p>';
});
// Create a fresh Nunjucks environment
var env = new nunjucks.Environment(null, { autoescape: false });
// Add template block to environement
var Ext = templateBlock.toNunjucksExt();
env.addExtension(templateBlock.getExtensionName(), new Ext());
// Render a template using the block
var src = '{% sayhello %}{% endsayhello %}';
return Promise.nfcall(env.renderString.bind(env), src)
.then(function(res) {
expect(res).toBe('<p>Hello, World!</p>');
});
});
pit('must apply block arguments correctly', function() {
var templateBlock = TemplateBlock.create('sayhello', function(block) {
return '<'+block.kwargs.tag+'>Hello, '+block.kwargs.name+'!</'+block.kwargs.tag+'>';
});
// Create a fresh Nunjucks environment
var env = new nunjucks.Environment(null, { autoescape: false });
// Add template block to environement
var Ext = templateBlock.toNunjucksExt();
env.addExtension(templateBlock.getExtensionName(), new Ext());
// Render a template using the block
var src = '{% sayhello name="Samy", tag="p" %}{% endsayhello %}';
return Promise.nfcall(env.renderString.bind(env), src)
.then(function(res) {
expect(res).toBe('<p>Hello, Samy!</p>');
});
});
pit('must handle nested blocks', function() {
var templateBlock = new TemplateBlock({
name: 'yoda',
blocks: Immutable.List(['start', 'end']),
process: function(block) {
var nested = {};
block.blocks.forEach(function(blk) {
nested[blk.name] = blk.body.trim();
});
return '<p class="yoda">'+nested.end+' '+nested.start+'</p>';
}
});
// Create a fresh Nunjucks environment
var env = new nunjucks.Environment(null, { autoescape: false });
// Add template block to environement
var Ext = templateBlock.toNunjucksExt();
env.addExtension(templateBlock.getExtensionName(), new Ext());
// Render a template using the block
var src = '{% yoda %}{% start %}this sentence should be{% end %}inverted{% endyoda %}';
return Promise.nfcall(env.renderString.bind(env), src)
.then(function(res) {
expect(res).toBe('<p class="yoda">inverted this sentence should be</p>');
});
});
});
});
+51
View File
@@ -0,0 +1,51 @@
describe('TemplateBlock', function() {
var TemplateEngine = require('../templateEngine');
describe('create', function() {
it('must initialize with a list of filters', function() {
var engine = TemplateEngine.create({
filters: {
hello: function(name) {
return 'Hello ' + name + '!';
}
}
});
var env = engine.toNunjucks();
var res = env.renderString('{{ "Luke"|hello }}');
expect(res).toBe('Hello Luke!');
});
it('must initialize with a list of globals', function() {
var engine = TemplateEngine.create({
globals: {
hello: function(name) {
return 'Hello ' + name + '!';
}
}
});
var env = engine.toNunjucks();
var res = env.renderString('{{ hello("Luke") }}');
expect(res).toBe('Hello Luke!');
});
it('must pass context to filters and blocks', function() {
var engine = TemplateEngine.create({
filters: {
hello: function(name) {
return 'Hello ' + name + ' ' + this.lastName + '!';
}
},
context: {
lastName: 'Skywalker'
}
});
var env = engine.toNunjucks();
var res = env.renderString('{{ "Luke"|hello }}');
expect(res).toBe('Hello Luke Skywalker!');
});
});
});
+258
View File
@@ -0,0 +1,258 @@
var path = require('path');
var Immutable = require('immutable');
var Ignore = require('ignore');
var Logger = require('../utils/logger');
var FS = require('./fs');
var Config = require('./config');
var Readme = require('./readme');
var Summary = require('./summary');
var Glossary = require('./glossary');
var Languages = require('./languages');
var Book = Immutable.Record({
// Logger for outptu message
logger: Logger(),
// Filesystem binded to the book scope to read files/directories
fs: FS(),
// Ignore files parser
ignore: Ignore(),
// Structure files
config: Config(),
readme: Readme(),
summary: Summary(),
glossary: Glossary(),
languages: Languages(),
// ID of the language for language books
language: String(),
// List of children, if multilingual (String -> Book)
books: Immutable.OrderedMap()
});
Book.prototype.getLogger = function() {
return this.get('logger');
};
Book.prototype.getFS = function() {
return this.get('fs');
};
Book.prototype.getIgnore = function() {
return this.get('ignore');
};
Book.prototype.getConfig = function() {
return this.get('config');
};
Book.prototype.getReadme = function() {
return this.get('readme');
};
Book.prototype.getSummary = function() {
return this.get('summary');
};
Book.prototype.getGlossary = function() {
return this.get('glossary');
};
Book.prototype.getLanguages = function() {
return this.get('languages');
};
Book.prototype.getBooks = function() {
return this.get('books');
};
Book.prototype.getLanguage = function() {
return this.get('language');
};
/**
Return FS instance to access the content
@return {FS}
*/
Book.prototype.getContentFS = function() {
var fs = this.getFS();
var config = this.getConfig();
var rootFolder = config.getValue('root');
if (rootFolder) {
return FS.reduceScope(fs, rootFolder);
}
return fs;
};
/**
Return root of the book
@return {String}
*/
Book.prototype.getRoot = function() {
var fs = this.getFS();
return fs.getRoot();
};
/**
Return root for content of the book
@return {String}
*/
Book.prototype.getContentRoot = function() {
var fs = this.getContentFS();
return fs.getRoot();
};
/**
Check if a file is ignore (should not being parsed, etc)
@param {String} ref
@return {Page|undefined}
*/
Book.prototype.isFileIgnored = function(filename) {
var ignore = this.getIgnore();
var language = this.getLanguage();
// Ignore is always relative to the root of the main book
if (language) {
filename = path.join(language, filename);
}
return ignore.filter([filename]).length == 0;
};
/**
Check if a content file is ignore (should not being parsed, etc)
@param {String} ref
@return {Page|undefined}
*/
Book.prototype.isContentFileIgnored = function(filename) {
var config = this.getConfig();
var rootFolder = config.getValue('root');
if (rootFolder) {
filename = path.join(rootFolder, filename);
}
return this.isFileIgnored(filename);
};
/**
Return a page from a book by its path
@param {String} ref
@return {Page|undefined}
*/
Book.prototype.getPage = function(ref) {
return this.getPages().get(ref);
};
/**
Is this book the parent of language's books
@return {Boolean}
*/
Book.prototype.isMultilingual = function() {
return (this.getLanguages().getCount() > 0);
};
/**
Return true if book is associated to a language
@return {Boolean}
*/
Book.prototype.isLanguageBook = function() {
return Boolean(this.getLanguage());
};
/**
Add a new language book
@param {String} language
@param {Book} book
@return {Book}
*/
Book.prototype.addLanguageBook = function(language, book) {
var books = this.getBooks();
books = books.set(language, book);
return this.set('books', books);
};
/**
Set the summary for this book
@param {Summary}
@return {Book}
*/
Book.prototype.setSummary = function(summary) {
return this.set('summary', summary);
};
/**
Set the readme for this book
@param {Readme}
@return {Book}
*/
Book.prototype.setReadme = function(readme) {
return this.set('readme', readme);
};
/**
Change log level
@param {String} level
@return {Book}
*/
Book.prototype.setLogLevel = function(level) {
this.getLogger().setLevel(level);
return this;
};
/**
Create a book using a filesystem
@param {FS} fs
@return {Book}
*/
Book.createForFS = function createForFS(fs) {
return new Book({
fs: fs
});
};
/**
Create a language book from a parent
@param {Book} parent
@param {String} language
@return {Book}
*/
Book.createFromParent = function createFromParent(parent, language) {
var ignore = parent.getIgnore();
return new Book({
// Inherits config. logegr and list of ignored files
logger: parent.getLogger(),
config: parent.getConfig(),
ignore: Ignore().add(ignore),
language: language,
fs: FS.reduceScope(parent.getContentFS(), language)
});
};
module.exports = Book;
+106
View File
@@ -0,0 +1,106 @@
var is = require('is');
var Immutable = require('immutable');
var File = require('./file');
var configDefault = require('../constants/configDefault');
var Config = Immutable.Record({
file: File(),
values: configDefault
}, 'Config');
Config.prototype.getFile = function() {
return this.get('file');
};
Config.prototype.getValues = function() {
return this.get('values');
};
/**
Return a configuration value by its key path
@param {String} key
@return {Mixed}
*/
Config.prototype.getValue = function(keyPath, def) {
var values = this.getValues();
keyPath = Config.keyToKeyPath(keyPath);
if (!values.hasIn(keyPath)) {
return Immutable.fromJS(def);
}
return values.getIn(keyPath);
};
/**
Update a configuration value
@param {String} key
@param {Mixed} value
@return {Mixed}
*/
Config.prototype.setValue = function(keyPath, value) {
keyPath = Config.keyToKeyPath(keyPath);
value = Immutable.fromJS(value);
var values = this.getValues();
values = values.setIn(keyPath, value);
return this.set('values', values);
};
/**
Create a new config for a file
@param {File} file
@param {Object} values
@returns {Config}
*/
Config.create = function(file, values) {
return new Config({
file: file,
values: Immutable.fromJS(values)
});
};
/**
Create a new config
@param {Object} values
@returns {Config}
*/
Config.createWithValues = function(values) {
return new Config({
values: Immutable.fromJS(values)
});
};
/**
Update values for an existing configuration
@param {Config} config
@param {Object} values
@returns {Config}
*/
Config.updateValues = function(config, values) {
values = Immutable.fromJS(values);
return config.set('values', values);
};
/**
Convert a keyPath to an array of keys
@param {String|Array}
@return {Array}
*/
Config.keyToKeyPath = function(keyPath) {
if (is.string(keyPath)) keyPath = keyPath.split('.');
return keyPath;
};
module.exports = Config;
+89
View File
@@ -0,0 +1,89 @@
var path = require('path');
var Immutable = require('immutable');
var parsers = require('../parsers');
var File = Immutable.Record({
// Path of the file, relative to the FS
path: String(),
// Time when file data last modified
mtime: Date()
});
File.prototype.getPath = function() {
return this.get('path');
};
File.prototype.getMTime = function() {
return this.get('mtime');
};
/**
Does the file exists / is set
@return {Boolean}
*/
File.prototype.exists = function() {
return Boolean(this.getPath());
};
/**
Return type of file ('markdown' or 'asciidoc')
@return {String}
*/
File.prototype.getType = function() {
var parser = this.getParser();
if (parser) {
return parser.name;
} else {
return undefined;
}
};
/**
Return extension of this file (lowercased)
@return {String}
*/
File.prototype.getExtension = function() {
return path.extname(this.getPath()).toLowerCase();
};
/**
Return parser for this file
@return {Parser}
*/
File.prototype.getParser = function() {
return parsers.getByExt(this.getExtension());
};
/**
Create a file from stats informations
@param {String} filepath
@param {Object|fs.Stats} stat
@return {File}
*/
File.createFromStat = function createFromStat(filepath, stat) {
return new File({
path: filepath,
mtime: stat.mtime
});
};
/**
Create a file with only a path
@param {String} filepath
@return {File}
*/
File.createWithFilepath = function createWithFilepath(filepath) {
return new File({
path: filepath
});
};
module.exports = File;
+274
View File
@@ -0,0 +1,274 @@
var path = require('path');
var Immutable = require('immutable');
var File = require('./file');
var Promise = require('../utils/promise');
var error = require('../utils/error');
var PathUtil = require('../utils/path');
var FS = Immutable.Record({
root: String(),
fsExists: Function(),
fsReadFile: Function(),
fsStatFile: Function(),
fsReadDir: Function(),
fsLoadObject: null
});
/**
Return path to the root
@return {String}
*/
FS.prototype.getRoot = function() {
return this.get('root');
};
/**
Verify that a file is in the fs scope
@param {String} filename
@return {Boolean}
*/
FS.prototype.isInScope = function(filename) {
var rootPath = this.getRoot();
filename = path.join(rootPath, filename);
return PathUtil.isInRoot(rootPath, filename);
};
/**
Resolve a file in this FS
@param {String}
@return {String}
*/
FS.prototype.resolve = function() {
var rootPath = this.getRoot();
var args = Array.prototype.slice.call(arguments);
var filename = path.join.apply(path, [rootPath].concat(args));
filename = path.normalize(filename);
if (!this.isInScope(filename)) {
throw error.FileOutOfScopeError({
filename: filename,
root: this.root
});
}
return filename;
};
/**
Check if a file exists, run a Promise(true) if that's the case, Promise(false) otherwise
@param {String} filename
@return {Promise<Boolean>}
*/
FS.prototype.exists = function(filename) {
var that = this;
return Promise()
.then(function() {
filename = that.resolve(filename);
var exists = that.get('fsExists');
return exists(filename);
});
};
/**
Read a file and returns a promise with the content as a buffer
@param {String} filename
@return {Promise<Buffer>}
*/
FS.prototype.read = function(filename) {
var that = this;
return Promise()
.then(function() {
filename = that.resolve(filename);
var read = that.get('fsReadFile');
return read(filename);
});
};
/**
Read a file as a string (utf-8)
@param {String} filename
@return {Promise<String>}
*/
FS.prototype.readAsString = function(filename, encoding) {
encoding = encoding || 'utf8';
return this.read(filename)
.then(function(buf) {
return buf.toString(encoding);
});
};
/**
Read stat infos about a file
@param {String} filename
@return {Promise<File>}
*/
FS.prototype.statFile = function(filename) {
var that = this;
return Promise()
.then(function() {
var filepath = that.resolve(filename);
var stat = that.get('fsStatFile');
return stat(filepath);
})
.then(function(stat) {
return File.createFromStat(filename, stat);
});
};
/**
List files/directories in a directory.
Directories ends with '/'
@param {String} dirname
@return {Promise<List<String>>}
*/
FS.prototype.readDir = function(dirname) {
var that = this;
return Promise()
.then(function() {
var dirpath = that.resolve(dirname);
var readDir = that.get('fsReadDir');
return readDir(dirpath);
})
.then(function(files) {
return Immutable.List(files);
});
};
/**
List only files in a diretcory
Directories ends with '/'
@param {String} dirname
@return {Promise<List<String>>}
*/
FS.prototype.listFiles = function(dirname) {
return this.readDir(dirname)
.then(function(files) {
return files.filterNot(pathIsFolder);
});
};
/**
List all files in a directory
@param {String} dirname
@return {Promise<List<String>>}
*/
FS.prototype.listAllFiles = function(folder) {
var that = this;
folder = folder || '.';
return this.readDir(folder)
.then(function(files) {
return Promise.reduce(files, function(out, file) {
var isDirectory = pathIsFolder(file);
if (!isDirectory) {
return out.push(path.join(folder, file));
}
return that.listAllFiles(path.join(folder, file))
.then(function(inner) {
return out.concat(inner);
});
}, Immutable.List());
});
};
/**
Find a file in a folder (case incensitive)
Return the found filename
@param {String} dirname
@param {String} filename
@return {Promise<String>}
*/
FS.prototype.findFile = function(dirname, filename) {
return this.listFiles(dirname)
.then(function(files) {
return files.find(function(file) {
return (file.toLowerCase() == filename.toLowerCase());
});
});
};
/**
Load a JSON file
By default, fs only supports JSON
@param {String} filename
@return {Promise<Object>}
*/
FS.prototype.loadAsObject = function(filename) {
var that = this;
var fsLoadObject = this.get('fsLoadObject');
return this.exists(filename)
.then(function(exists) {
if (!exists) {
var err = new Error('Module doesn\'t exist');
err.code = 'MODULE_NOT_FOUND';
throw err;
}
if (fsLoadObject) {
return fsLoadObject(that.resolve(filename));
} else {
return that.readAsString(filename)
.then(function(str) {
return JSON.parse(str);
});
}
});
};
/**
Create a FS instance
@param {Object} def
@return {FS}
*/
FS.create = function create(def) {
return new FS(def);
};
/**
Create a new FS instance with a reduced scope
@param {FS} fs
@param {String} scope
@return {FS}
*/
FS.reduceScope = function reduceScope(fs, scope) {
return fs.set('root', path.join(fs.getRoot(), scope));
};
// .readdir return files/folder as a list of string, folder ending with '/'
function pathIsFolder(filename) {
var lastChar = filename[filename.length - 1];
return lastChar == '/' || lastChar == '\\';
}
module.exports = FS;
+109
View File
@@ -0,0 +1,109 @@
var Immutable = require('immutable');
var error = require('../utils/error');
var File = require('./file');
var GlossaryEntry = require('./glossaryEntry');
var parsers = require('../parsers');
var Glossary = Immutable.Record({
file: File(),
entries: Immutable.OrderedMap()
});
Glossary.prototype.getFile = function() {
return this.get('file');
};
Glossary.prototype.getEntries = function() {
return this.get('entries');
};
/**
Return an entry by its name
@param {String} name
@return {GlossaryEntry}
*/
Glossary.prototype.getEntry = function(name) {
var entries = this.getEntries();
var id = GlossaryEntry.nameToID(name);
return entries.get(id);
};
/**
Render glossary as text
@return {Promise<String>}
*/
Glossary.prototype.toText = function(parser) {
var file = this.getFile();
var entries = this.getEntries();
parser = parser? parsers.getByExt(parser) : file.getParser();
if (!parser) {
throw error.FileNotParsableError({
filename: file.getPath()
});
}
return parser.glossary.toText(entries.toJS());
};
/**
Add/Replace an entry to a glossary
@param {Glossary} glossary
@param {GlossaryEntry} entry
@return {Glossary}
*/
Glossary.addEntry = function addEntry(glossary, entry) {
var id = entry.getID();
var entries = glossary.getEntries();
entries = entries.set(id, entry);
return glossary.set('entries', entries);
};
/**
Add/Replace an entry to a glossary by name/description
@param {Glossary} glossary
@param {GlossaryEntry} entry
@return {Glossary}
*/
Glossary.addEntryByName = function addEntryByName(glossary, name, description) {
var entry = new GlossaryEntry({
name: name,
description: description
});
return Glossary.addEntry(glossary, entry);
};
/**
Create a glossary from a list of entries
@param {String} filename
@param {Array|List} entries
@return {Glossary}
*/
Glossary.createFromEntries = function createFromEntries(file, entries) {
entries = entries.map(function(entry) {
if (!(entry instanceof GlossaryEntry)) {
entry = new GlossaryEntry(entry);
}
return [entry.getID(), entry];
});
return new Glossary({
file: file,
entries: Immutable.OrderedMap(entries)
});
};
module.exports = Glossary;
+43
View File
@@ -0,0 +1,43 @@
var Immutable = require('immutable');
var slug = require('github-slugid');
/*
A definition represents an entry in the glossary
*/
var GlossaryEntry = Immutable.Record({
name: String(),
description: String()
});
GlossaryEntry.prototype.getName = function() {
return this.get('name');
};
GlossaryEntry.prototype.getDescription = function() {
return this.get('description');
};
/**
Get identifier for this entry
@retrun {Boolean}
*/
GlossaryEntry.prototype.getID = function() {
return GlossaryEntry.nameToID(this.getName());
};
/**
Normalize a glossary entry name into a unique id
@param {String}
@return {String}
*/
GlossaryEntry.nameToID = function nameToID(name) {
return slug(name);
};
module.exports = GlossaryEntry;
+21
View File
@@ -0,0 +1,21 @@
var path = require('path');
var Immutable = require('immutable');
var Language = Immutable.Record({
title: String(),
path: String()
});
Language.prototype.getTitle = function() {
return this.get('title');
};
Language.prototype.getPath = function() {
return this.get('path');
};
Language.prototype.getID = function() {
return path.basename(this.getPath());
};
module.exports = Language;
+71
View File
@@ -0,0 +1,71 @@
var Immutable = require('immutable');
var File = require('./file');
var Language = require('./language');
var Languages = Immutable.Record({
file: File(),
list: Immutable.OrderedMap()
});
Languages.prototype.getFile = function() {
return this.get('file');
};
Languages.prototype.getList = function() {
return this.get('list');
};
/**
Get default languages
@return {Language}
*/
Languages.prototype.getDefaultLanguage = function() {
return this.getList().first();
};
/**
Get a language by its ID
@param {String} lang
@return {Language}
*/
Languages.prototype.getLanguage = function(lang) {
return this.getList().get(lang);
};
/**
Return count of langs
@return {Number}
*/
Languages.prototype.getCount = function() {
return this.getList().size;
};
/**
Create a languages list from a JS object
@param {File}
@param {Array}
@return {Language}
*/
Languages.createFromList = function(file, langs) {
var list = Immutable.OrderedMap();
langs.forEach(function(lang) {
lang = Language({
title: lang.title,
path: lang.path
});
list = list.set(lang.getID(), lang);
});
return Languages({
file: file,
list: list
});
};
module.exports = Languages;
+93
View File
@@ -0,0 +1,93 @@
var Immutable = require('immutable');
var Book = require('./book');
var Output = Immutable.Record({
book: Book(),
// Name of the generator being used
generator: String(),
// Map of plugins to use (String -> Plugin)
plugins: Immutable.OrderedMap(),
// Map pages to generation (String -> Page)
pages: Immutable.OrderedMap(),
// List assets (String)
assets: Immutable.List(),
// Option for the generation
options: Immutable.Map(),
// Internal state for the generation
state: Immutable.Map()
});
Output.prototype.getBook = function() {
return this.get('book');
};
Output.prototype.getGenerator = function() {
return this.get('generator');
};
Output.prototype.getPlugins = function() {
return this.get('plugins');
};
Output.prototype.getPages = function() {
return this.get('pages');
};
Output.prototype.getOptions = function() {
return this.get('options');
};
Output.prototype.getAssets = function() {
return this.get('assets');
};
Output.prototype.getState = function() {
return this.get('state');
};
/**
Get root folder for output
@return {String}
*/
Output.prototype.getRoot = function() {
return this.getOptions().get('root');
};
/**
Update state of output
@param {Map} newState
@return {Output}
*/
Output.prototype.setState = function(newState) {
return this.set('state', newState);
};
/**
Update options
@param {Map} newOptions
@return {Output}
*/
Output.prototype.setOptions = function(newOptions) {
return this.set('options', newOptions);
};
/**
Return logegr for this output (same as book)
@return {Logger}
*/
Output.prototype.getLogger = function() {
return this.getBook().getLogger();
};
module.exports = Output;
+55
View File
@@ -0,0 +1,55 @@
var Immutable = require('immutable');
var File = require('./file');
var Page = Immutable.Record({
file: File(),
// Attributes extracted from the YAML header
attributes: Immutable.Map(),
// Content of the page
content: String(),
// Direction of the text
dir: String('ltr')
});
Page.prototype.getFile = function() {
return this.get('file');
};
Page.prototype.getAttributes = function() {
return this.get('attributes');
};
Page.prototype.getContent = function() {
return this.get('content');
};
Page.prototype.getDir = function() {
return this.get('dir');
};
/**
Return path of the page
@return {String}
*/
Page.prototype.getPath = function() {
return this.getFile().getPath();
};
/**
Create a page for a file
@param {File} file
@return {Page}
*/
Page.createForFile = function(file) {
return new Page({
file: file
});
};
module.exports = Page;
+152
View File
@@ -0,0 +1,152 @@
var Immutable = require('immutable');
var TemplateBlock = require('./templateBlock');
var PREFIX = require('../constants/pluginPrefix');
var DEFAULT_VERSION = '*';
var Plugin = Immutable.Record({
name: String(),
// Requirement version (ex: ">1.0.0")
version: String(DEFAULT_VERSION),
// Path to load this plugin
path: String(),
// Depth of this plugin in the dependency tree
depth: Number(0),
// Content of the "package.json"
package: Immutable.Map(),
// Content of the package itself
content: Immutable.Map()
}, 'Plugin');
Plugin.prototype.getName = function() {
return this.get('name');
};
Plugin.prototype.getPath = function() {
return this.get('path');
};
Plugin.prototype.getVersion = function() {
return this.get('version');
};
Plugin.prototype.getPackage = function() {
return this.get('package');
};
Plugin.prototype.getContent = function() {
return this.get('content');
};
Plugin.prototype.getDepth = function() {
return this.get('depth');
};
/**
Return the ID on NPM for this plugin
@return {String}
*/
Plugin.prototype.getNpmID = function() {
return Plugin.nameToNpmID(this.getName());
};
/**
Check if a plugin is loaded
@return {Boolean}
*/
Plugin.prototype.isLoaded = function() {
return Boolean(this.getPackage().size > 0);
};
/**
Return map of hooks
@return {Map<String:Function>}
*/
Plugin.prototype.getHooks = function() {
return this.getContent().get('hooks') || Immutable.Map();
};
/**
Return infos about resources for a specific type
@param {String} type
@return {Map<String:Mixed>}
*/
Plugin.prototype.getResources = function(type) {
if (type != 'website' && type != 'ebook') {
throw new Error('Invalid assets type ' + type);
}
var content = this.getContent();
return (content.get(type)
|| (type == 'website'? content.get('book') : null)
|| Immutable.Map());
};
/**
Return map of filters
@return {Map<String:Function>}
*/
Plugin.prototype.getFilters = function() {
return this.getContent().get('filters');
};
/**
Return map of blocks
@return {Map<String:TemplateBlock>}
*/
Plugin.prototype.getBlocks = function() {
var blocks = this.getContent().get('blocks');
blocks = blocks || Immutable.Map();
return blocks
.map(function(block, blockName) {
return TemplateBlock.create(blockName, block);
});
};
/**
Return a specific hook
@param {String} name
@return {Function|undefined}
*/
Plugin.prototype.getHook = function(name) {
return this.getHooks().get(name);
};
/**
Create a plugin from a string
@param {String}
@return {Plugin}
*/
Plugin.createFromString = function(s) {
var parts = s.split('@');
var name = parts[0];
var version = parts.slice(1).join('@');
return new Plugin({
name: name,
version: version || DEFAULT_VERSION
});
};
/**
Return NPM id for a plugin name
@param {String}
@return {String}
*/
Plugin.nameToNpmID = function(s) {
return PREFIX + s;
};
module.exports = Plugin;
+40
View File
@@ -0,0 +1,40 @@
var Immutable = require('immutable');
var File = require('./file');
var Readme = Immutable.Record({
file: File(),
title: String(),
description: String()
});
Readme.prototype.getFile = function() {
return this.get('file');
};
Readme.prototype.getTitle = function() {
return this.get('title');
};
Readme.prototype.getDescription = function() {
return this.get('description');
};
/**
Create a new readme
@param {File} file
@param {Object} def
@return {Readme}
*/
Readme.create = function(file, def) {
def = def || {};
return new Readme({
file: file,
title: def.title || '',
description: def.description || ''
});
};
module.exports = Readme;
+190
View File
@@ -0,0 +1,190 @@
var is = require('is');
var Immutable = require('immutable');
var error = require('../utils/error');
var LocationUtils = require('../utils/location');
var File = require('./file');
var SummaryPart = require('./summaryPart');
var SummaryArticle = require('./summaryArticle');
var parsers = require('../parsers');
var Summary = Immutable.Record({
file: File(),
parts: Immutable.List()
}, 'Summary');
Summary.prototype.getFile = function() {
return this.get('file');
};
Summary.prototype.getParts = function() {
return this.get('parts');
};
/**
Return a part by its index
@param {Number}
@return {Part}
*/
Summary.prototype.getPart = function(i) {
var parts = this.getParts();
return parts.get(i);
};
/**
Return an article using an iterator to find it.
if "partIter" is set, it can also return a Part.
@param {Function} iter
@param {Function} partIter
@return {Article|Part}
*/
Summary.prototype.getArticle = function(iter, partIter) {
var parts = this.getParts();
return parts.reduce(function(result, part) {
if (result) return result;
if (partIter && partIter(part)) return part;
return SummaryArticle.findArticle(part, iter);
}, null);
};
/**
Return a part/article by its level
@param {String} level
@return {Article}
*/
Summary.prototype.getByLevel = function(level) {
function iterByLevel(article) {
return (article.getLevel() === level);
}
return this.getArticle(iterByLevel, iterByLevel);
};
/**
Return an article by its path
@param {String} filePath
@return {Article}
*/
Summary.prototype.getByPath = function(filePath) {
return this.getArticle(function(article) {
return (LocationUtils.areIdenticalPaths(article.getPath(), filePath));
});
};
/**
Return the first article
@return {Article}
*/
Summary.prototype.getFirstArticle = function() {
return this.getArticle(function(article) {
return true;
});
};
/**
Return next article of an article
@param {Article} current
@return {Article}
*/
Summary.prototype.getNextArticle = function(current) {
var level = is.string(current)? current : current.getLevel();
var wasPrev = false;
return this.getArticle(function(article) {
if (wasPrev) return true;
wasPrev = article.getLevel() == level;
return false;
});
};
/**
Return previous article of an article
@param {Article} current
@return {Article}
*/
Summary.prototype.getPrevArticle = function(current) {
var level = is.string(current)? current : current.getLevel();
var prev = undefined;
this.getArticle(function(article) {
if (article.getLevel() == level) {
return true;
}
prev = article;
return false;
});
return prev;
};
/**
Render summary as text
@return {Promise<String>}
*/
Summary.prototype.toText = function(parser) {
var file = this.getFile();
var parts = this.getParts();
parser = parser? parsers.getByExt(parser) : file.getParser();
if (!parser) {
throw error.FileNotParsableError({
filename: file.getPath()
});
}
return parser.summary.toText({
parts: parts.toJS()
});
};
/**
Return all articles as a list
@return {List<Article>}
*/
Summary.prototype.getArticlesAsList = function() {
var accu = [];
this.getArticle(function(article) {
accu.push(article);
});
return Immutable.List(accu);
};
/**
Create a new summary for a list of parts
@param {Lust|Array} parts
@return {Summary}
*/
Summary.createFromParts = function createFromParts(file, parts) {
parts = parts.map(function(part, i) {
if (part instanceof SummaryPart) {
return part;
}
return SummaryPart.create(part, i + 1);
});
return new Summary({
file: file,
parts: new Immutable.List(parts)
});
};
module.exports = Summary;
+150
View File
@@ -0,0 +1,150 @@
var Immutable = require('immutable');
var location = require('../utils/location');
/*
An article represents an entry in the Summary / table of Contents
*/
var SummaryArticle = Immutable.Record({
level: String(),
title: String(),
ref: String(),
articles: Immutable.List()
}, 'SummaryArticle');
SummaryArticle.prototype.getLevel = function() {
return this.get('level');
};
SummaryArticle.prototype.getTitle = function() {
return this.get('title');
};
SummaryArticle.prototype.getRef = function() {
return this.get('ref');
};
SummaryArticle.prototype.getArticles = function() {
return this.get('articles');
};
/**
Return how deep the article is
@return {Number}
*/
SummaryArticle.prototype.getDepth = function() {
return this.getLevel().split('.').length;
};
/**
Get path (without anchor) to the pointing file
@return {String}
*/
SummaryArticle.prototype.getPath = function() {
if (this.isExternal()) {
return undefined;
}
var ref = this.getRef();
if (!ref) {
return undefined;
}
var parts = ref.split('#');
var pathname = (parts.length > 1? parts.slice(0, -1).join('#') : ref);
// Normalize path to remove ('./', etc)
return location.normalize(pathname);
};
/**
Return url if article is external
@return {String}
*/
SummaryArticle.prototype.getUrl = function() {
return this.isExternal()? this.getRef() : undefined;
};
/**
Get anchor for this article (or undefined)
@return {String}
*/
SummaryArticle.prototype.getAnchor = function() {
var ref = this.getRef();
var parts = ref.split('#');
var anchor = (parts.length > 1? '#' + parts[parts.length - 1] : undefined);
return anchor;
};
/**
Is article pointing to a page of an absolute url
@return {Boolean}
*/
SummaryArticle.prototype.isPage = function() {
return !this.isExternal() && this.getRef();
};
/**
Is article pointing to aan absolute url
@return {Boolean}
*/
SummaryArticle.prototype.isExternal = function() {
return location.isExternal(this.getRef());
};
/**
Create a SummaryArticle
@param {Object} def
@return {SummaryArticle}
*/
SummaryArticle.create = function(def, level) {
var articles = (def.articles || []).map(function(article, i) {
if (article instanceof SummaryArticle) {
return article;
}
return SummaryArticle.create(article, [level, i + 1].join('.'));
});
return new SummaryArticle({
level: level,
title: def.title,
ref: def.ref || def.path || '',
articles: Immutable.List(articles)
});
};
/**
Find an article from a base one
@param {Article|Part} base
@param {Function(article)} iter
@return {Article}
*/
SummaryArticle.findArticle = function(base, iter) {
var articles = base.getArticles();
return articles.reduce(function(result, article) {
if (result) return result;
if (iter(article)) {
return article;
}
return SummaryArticle.findArticle(article, iter);
}, null);
};
module.exports = SummaryArticle;
+48
View File
@@ -0,0 +1,48 @@
var Immutable = require('immutable');
var SummaryArticle = require('./summaryArticle');
/*
A part represents a section in the Summary / table of Contents
*/
var SummaryPart = Immutable.Record({
level: String(),
title: String(),
articles: Immutable.List()
});
SummaryPart.prototype.getLevel = function() {
return this.get('level');
};
SummaryPart.prototype.getTitle = function() {
return this.get('title');
};
SummaryPart.prototype.getArticles = function() {
return this.get('articles');
};
/**
Create a SummaryPart
@param {Object} def
@return {SummaryPart}
*/
SummaryPart.create = function(def, level) {
var articles = (def.articles || []).map(function(article, i) {
if (article instanceof SummaryArticle) {
return article;
}
return SummaryArticle.create(article, [level, i + 1].join('.'));
});
return new SummaryPart({
level: String(level),
title: def.title,
articles: Immutable.List(articles)
});
};
module.exports = SummaryPart;
+310
View File
@@ -0,0 +1,310 @@
var is = require('is');
var extend = require('extend');
var Immutable = require('immutable');
var Promise = require('../utils/promise');
var genKey = require('../utils/genKey');
var NODE_ENDARGS = '%%endargs%%';
var blockBodies = {};
var TemplateBlock = Immutable.Record({
// Name of block, also the start tag
name: String(),
// End tag, default to "end<name>"
end: String(),
// Function to process the block content
process: Function(),
// List of String, for inner block tags
blocks: Immutable.List(),
// List of shortcuts to replace with this block
shortcuts: Immutable.List(),
// Function to execute in post processing
post: null,
parse: true
}, 'TemplateBlock');
TemplateBlock.prototype.getName = function() {
return this.get('name');
};
TemplateBlock.prototype.getPost = function() {
return this.get('post');
};
TemplateBlock.prototype.getParse = function() {
return this.get('parse');
};
TemplateBlock.prototype.getEndTag = function() {
return this.get('end') || ('end' + this.getName());
};
TemplateBlock.prototype.getProcess = function() {
return this.get('process');
};
TemplateBlock.prototype.getBlocks = function() {
return this.get('blocks');
};
TemplateBlock.prototype.getShortcuts = function() {
return this.get('shortcuts');
};
/**
Return name for the nunjucks extension
@return {String}
*/
TemplateBlock.prototype.getExtensionName = function() {
return 'Block' + this.getName() + 'Extension';
};
/**
Return a nunjucks extension to represents this block
@return {Nunjucks.Extension}
*/
TemplateBlock.prototype.toNunjucksExt = function(mainContext) {
var that = this;
var name = this.getName();
var endTag = this.getEndTag();
var blocks = this.getBlocks().toJS();
function Ext() {
this.tags = [name];
this.parse = function(parser, nodes) {
var lastBlockName = null;
var lastBlockArgs = null;
var allBlocks = blocks.concat([endTag]);
// Parse first block
var tok = parser.nextToken();
lastBlockArgs = parser.parseSignature(null, true);
parser.advanceAfterBlockEnd(tok.value);
var args = new nodes.NodeList();
var bodies = [];
var blockNamesNode = new nodes.Array(tok.lineno, tok.colno);
var blockArgCounts = new nodes.Array(tok.lineno, tok.colno);
// Parse while we found "end<block>"
do {
// Read body
var currentBody = parser.parseUntilBlocks.apply(parser, allBlocks);
// Handle body with previous block name and args
blockNamesNode.addChild(new nodes.Literal(args.lineno, args.colno, lastBlockName));
blockArgCounts.addChild(new nodes.Literal(args.lineno, args.colno, lastBlockArgs.children.length));
bodies.push(currentBody);
// Append arguments of this block as arguments of the run function
lastBlockArgs.children.forEach(function(child) {
args.addChild(child);
});
// Read new block
lastBlockName = parser.nextToken().value;
// Parse signature and move to the end of the block
if (lastBlockName != endTag) {
lastBlockArgs = parser.parseSignature(null, true);
}
parser.advanceAfterBlockEnd(lastBlockName);
} while (lastBlockName != endTag);
args.addChild(blockNamesNode);
args.addChild(blockArgCounts);
args.addChild(new nodes.Literal(args.lineno, args.colno, NODE_ENDARGS));
return new nodes.CallExtensionAsync(this, 'run', args, bodies);
};
this.run = function(context) {
var fnArgs = Array.prototype.slice.call(arguments, 1);
var args;
var blocks = [];
var bodies = [];
var blockNames;
var blockArgCounts;
var callback;
// Extract callback
callback = fnArgs.pop();
// Detect end of arguments
var endArgIndex = fnArgs.indexOf(NODE_ENDARGS);
// Extract arguments and bodies
args = fnArgs.slice(0, endArgIndex);
bodies = fnArgs.slice(endArgIndex + 1);
// Extract block counts
blockArgCounts = args.pop();
blockNames = args.pop();
// Recreate list of blocks
blockNames.forEach(function(name, i) {
var countArgs = blockArgCounts[i];
var blockBody = bodies.shift();
var blockArgs = countArgs > 0? args.slice(0, countArgs) : [];
args = args.slice(countArgs);
var blockKwargs = extractKwargs(blockArgs);
blocks.push({
name: name,
body: blockBody(),
args: blockArgs,
kwargs: blockKwargs
});
});
var mainBlock = blocks.shift();
mainBlock.blocks = blocks;
Promise()
.then(function() {
var ctx = extend({
ctx: context
}, mainContext || {});
return that.applyBlock(mainBlock, ctx);
})
.then(function(result) {
return that.blockResultToHtml(result);
})
.nodeify(callback);
};
};
return Ext;
};
/**
Apply a block to a content
@param {Object} inner
@param {Object} context
@return {Promise<String>|String}
*/
TemplateBlock.prototype.applyBlock = function(inner, context) {
var processFn = this.getProcess();
inner = inner || {};
inner.args = inner.args || [];
inner.kwargs = inner.kwargs || {};
inner.blocks = inner.blocks || [];
var r = processFn.call(context, inner);
if (Promise.isPromiseAlike(r)) {
return r.then(this.handleBlockResult);
} else {
return this.handleBlockResult(r);
}
};
/**
Handle result from a block process function
@param {Object} result
@return {Object}
*/
TemplateBlock.prototype.handleBlockResult = function(result) {
if (is.string(result)) {
result = { body: result };
}
result.name = this.getName();
return result;
};
/**
Convert a block result to HTML
@param {Object} result
@return {String}
*/
TemplateBlock.prototype.blockResultToHtml = function(result) {
var parse = this.getParse();
var indexedKey;
var toIndex = (!parse) || (this.getPost() !== undefined);
if (toIndex) {
indexedKey = TemplateBlock.indexBlockResult(result);
}
// Parsable block, just return it
if (parse) {
return result.body;
}
// Return it as a position marker
return '{{-%' + indexedKey + '%-}}';
};
/**
Index a block result, and return the indexed key
@param {Object} blk
@return {String}
*/
TemplateBlock.indexBlockResult = function(blk) {
var key = genKey();
blockBodies[key] = blk;
return key;
};
/**
Get a block results indexed for a specific key
@param {String} key
@return {Object|undefined}
*/
TemplateBlock.getBlockResultByKey = function(key) {
return blockBodies[key];
};
/**
Create a template block from a function or an object
@param {String} blockName
@param {Object} block
@return {TemplateBlock}
*/
TemplateBlock.create = function(blockName, block) {
if (is.fn(block)) {
block = new Immutable.Map({
process: block
});
}
block = block.set('name', blockName);
return new TemplateBlock(block);
};
/**
Extract kwargs from an arguments array
@param {Array} args
@return {Object}
*/
function extractKwargs(args) {
var last = args[args.length - 1];
return (is.object(last) && last.__keywords)? args.pop() : {};
}
module.exports = TemplateBlock;
+139
View File
@@ -0,0 +1,139 @@
var nunjucks = require('nunjucks');
var Immutable = require('immutable');
var TemplateEngine = Immutable.Record({
// Map of {TemplateBlock}
blocks: Immutable.Map(),
// Map of Extension
extensions: Immutable.Map(),
// Map of filters: {String} name -> {Function} fn
filters: Immutable.Map(),
// Map of globals: {String} name -> {Mixed}
globals: Immutable.Map(),
// Context for filters / blocks
context: Object(),
// Nunjucks loader
loader: nunjucks.FileSystemLoader('views')
}, 'TemplateEngine');
TemplateEngine.prototype.getBlocks = function() {
return this.get('blocks');
};
TemplateEngine.prototype.getGlobals = function() {
return this.get('globals');
};
TemplateEngine.prototype.getFilters = function() {
return this.get('filters');
};
TemplateEngine.prototype.getShortcuts = function() {
return this.get('shortcuts');
};
TemplateEngine.prototype.getLoader = function() {
return this.get('loader');
};
TemplateEngine.prototype.getContext = function() {
return this.get('context');
};
TemplateEngine.prototype.getExtensions = function() {
return this.get('extensions');
};
/**
Return a block by its name (or undefined)
@param {String} name
@return {TemplateBlock}
*/
TemplateEngine.prototype.getBlock = function(name) {
var blocks = this.getBlocks();
return blocks.find(function(block) {
return block.getName() === name;
});
};
/**
Return a nunjucks environment from this configuration
@return {Nunjucks.Environment}
*/
TemplateEngine.prototype.toNunjucks = function() {
var loader = this.getLoader();
var blocks = this.getBlocks();
var filters = this.getFilters();
var globals = this.getGlobals();
var extensions = this.getExtensions();
var context = this.getContext();
var env = new nunjucks.Environment(
loader,
{
// Escaping is done after by the asciidoc/markdown parser
autoescape: false,
// Syntax
tags: {
blockStart: '{%',
blockEnd: '%}',
variableStart: '{{',
variableEnd: '}}',
commentStart: '{###',
commentEnd: '###}'
}
}
);
// Add filters
filters.forEach(function(filterFn, filterName) {
env.addFilter(filterName, filterFn.bind(context));
});
// Add blocks
blocks.forEach(function(block) {
var extName = block.getExtensionName();
var Ext = block.toNunjucksExt(context);
env.addExtension(extName, new Ext());
});
// Add globals
globals.forEach(function(globalValue, globalName) {
env.addGlobal(globalName, globalValue);
});
// Add other extensions
extensions.forEach(function(ext, extName) {
env.addExtension(extName, ext);
});
return env;
};
/**
Create a template engine
@param {Object} def
@return {TemplateEngine}
*/
TemplateEngine.create = function(def) {
return new TemplateEngine({
blocks: Immutable.List(def.blocks || []),
extensions: Immutable.Map(def.extensions || {}),
filters: Immutable.Map(def.filters || {}),
globals: Immutable.Map(def.globals || {}),
context: def.context,
loader: def.loader
});
};
module.exports = TemplateEngine;
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
Summary: require('./summary')
};
@@ -0,0 +1,44 @@
var Summary = require('../../../models/summary');
var File = require('../../../models/file');
describe('editPartTitle', function() {
var editPartTitle = require('../editPartTitle');
var summary = Summary.createFromParts(File(), [
{
articles: [
{
title: 'My First Article',
path: 'README.md'
},
{
title: 'My Second Article',
path: 'article.md'
}
]
},
{
title: 'Test'
}
]);
it('should correctly set title of first part', function() {
var newSummary = editPartTitle(summary, 0, 'Hello World');
var part = newSummary.getPart(0);
expect(part.getTitle()).toBe('Hello World');
});
it('should correctly set title of second part', function() {
var newSummary = editPartTitle(summary, 1, 'Hello');
var part = newSummary.getPart(1);
expect(part.getTitle()).toBe('Hello');
});
it('should not fail if part doesn\'t exist', function() {
var newSummary = editPartTitle(summary, 3, 'Hello');
expect(newSummary.getParts().size).toBe(2);
});
});

Some files were not shown because too many files have changed in this diff Show More