mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-24 19:32:07 +00:00
Merge branch 'fixes'
This commit is contained in:
+3
-1
@@ -1,2 +1,4 @@
|
||||
docs/**/*
|
||||
test/node_modules/**/*
|
||||
_book/**/*
|
||||
node_modules/**/*
|
||||
test/**/*
|
||||
|
||||
@@ -12,8 +12,9 @@
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
"mocha": true,
|
||||
"browser": true
|
||||
"browser": true,
|
||||
"jest": true,
|
||||
"jasmine": true
|
||||
},
|
||||
"extends": "eslint:recommended"
|
||||
}
|
||||
@@ -3,7 +3,6 @@ language: node_js
|
||||
node_js:
|
||||
- "stable"
|
||||
- "4.1"
|
||||
- "0.12"
|
||||
before_install:
|
||||
- npm install svgexport -g
|
||||
after_success:
|
||||
|
||||
+2
-2
@@ -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:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# GitBook Toolchain Documentation
|
||||
|
||||

|
||||
|
||||
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?
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
var gitbook = require('../gitbook');
|
||||
|
||||
describe('satisfies', function() {
|
||||
|
||||
it('should return true for *', function() {
|
||||
expect(gitbook.satisfies('*')).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
describe('GitBook', function() {
|
||||
it('should correctly export', function() {
|
||||
require('../');
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
module.exports = {
|
||||
encodePage: require('./encodePage'),
|
||||
decodePage: require('./decodePage'),
|
||||
|
||||
encodeGlobal: require('./encodeGlobal'),
|
||||
decodeGlobal: require('./decodeGlobal')
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,8 +0,0 @@
|
||||
|
||||
module.exports = {
|
||||
Readme: require('./readme'),
|
||||
Summary: require('./summary'),
|
||||
Glossary: require('./glossary'),
|
||||
Langs: require('./langs')
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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;
|
||||
@@ -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
|
||||
};
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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')
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
var Immutable = require('immutable');
|
||||
var jsonSchemaDefaults = require('json-schema-defaults');
|
||||
|
||||
var schema = require('./configSchema');
|
||||
|
||||
module.exports = Immutable.fromJS(jsonSchemaDefaults(schema));
|
||||
@@ -0,0 +1,5 @@
|
||||
// Configuration files to test (sorted)
|
||||
module.exports = [
|
||||
'book.js',
|
||||
'book.json'
|
||||
];
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -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'
|
||||
]);
|
||||
@@ -0,0 +1,6 @@
|
||||
// Files containing ignore pattner (sorted by priority)
|
||||
module.exports = [
|
||||
'.ignore',
|
||||
'.gitignore',
|
||||
'.bookignore'
|
||||
];
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
module.exports = '_assets';
|
||||
@@ -0,0 +1,8 @@
|
||||
module.exports = [
|
||||
'init',
|
||||
'finish',
|
||||
'finish:before',
|
||||
'config',
|
||||
'page',
|
||||
'page:before'
|
||||
];
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
/*
|
||||
All GitBook plugins are NPM packages starting with this prefix.
|
||||
*/
|
||||
module.exports = 'gitbook-plugin-';
|
||||
@@ -0,0 +1,6 @@
|
||||
var Immutable = require('immutable');
|
||||
|
||||
module.exports = Immutable.List([
|
||||
'js',
|
||||
'css'
|
||||
]);
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
module.exports = '_layouts';
|
||||
@@ -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
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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')
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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>');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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!');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
Reference in New Issue
Block a user