Compare commits

..

79 Commits

Author SHA1 Message Date
Samy Pessé ce50a1d50f Bump version to 3.0.0-pre.14 2016-05-10 14:37:22 +02:00
Samy Pessé a76e018d36 Update theme-default@1.0.0-pre.10 2016-05-10 14:37:05 +02:00
Samy Pessé b06f1fd677 Fix article.depth starting at 2 2016-05-10 14:36:03 +02:00
Samy Pessé 3d8b1880dc Fix page's title in pdf header/footer 2016-05-10 14:25:50 +02:00
Samy Pessé 1a13c8c9bb Add option "--no-live" for command serve 2016-05-10 13:14:32 +02:00
Samy Pessé 852fae3a09 Fix #1232: add option "--no-watch" to disable watcher and live reloading 2016-05-10 13:08:19 +02:00
Samy Pessé d342d31c8e Fix error when summary contains entry without ref 2016-05-10 12:12:19 +02:00
Samy Pesse 5b3f54fbc8 Fix "gitbook init" when article has no ref 2016-05-10 09:55:22 +02:00
Samy Pessé 308ece784c Merge pull request #1288 from todvora/patch-1
Fixed summary encoding
2016-05-10 08:55:40 +02:00
Tomas Dvorak 0758ddf04e Fixed summary encoding 2016-05-10 08:17:52 +02:00
Samy Pesse f24d9aef73 Add test for block replaced with anchors 2016-05-09 22:17:46 +02:00
Samy Pesse f8bb55f258 Adapt unit tests for blocks 2016-05-09 22:01:25 +02:00
Samy Pesse 76daebeb84 Fix unit test for templateBlock using a removed method "getParse" 2016-05-09 18:41:21 +02:00
Samy Pesse b0d366b7e8 Fix linting warning in models/templateBlock.js 2016-05-08 22:45:26 +02:00
Samy Pesse 3d0054dd64 Fix #1286: by default blocks should not be parsable 2016-05-08 22:33:41 +02:00
Samy Pessé 13bc89ff44 Merge pull request #1285 from GitbookIO/api/summary
[WIP] Api for plugins to access summary
2016-05-08 22:28:15 +02:00
Samy Pesse bb910f984e Improve comment for API summary.walk 2016-05-08 22:26:46 +02:00
Samy Pessé e576fdea14 Merge pull request #1278 from dylang/patch-1
bump bash-color to version that has a license
2016-05-08 18:44:53 +02:00
Samy Pesse c7864927ec Add method summary.getArticleByPath to summary api 2016-05-08 11:08:27 +02:00
Samy Pesse 54956de59f Start API for plugins to access the summary 2016-05-08 10:33:45 +02:00
Samy Pesse 2cbd30d1f2 Fix command "gitbook init" 2016-05-07 00:24:12 +02:00
Dylan Greene 55614f1675 bump bash-color to version that has a license
Version currently being used does not have a license making it difficult for companies to use.
2016-05-06 09:56:43 -04:00
Samy Pesse 4f7421993f Bump version to 3.0.0-pre.13 2016-05-06 10:59:52 +02:00
Samy Pesse 32ad7b530e Fix #1266: don't fail on "a" tags without "href" 2016-05-06 10:57:41 +02:00
Samy Pesse ee5d5010f6 Correctly read assets using input fs when copying 2016-05-06 10:54:13 +02:00
Samy Pesse cf7e5884cd Add method readAsStream on fs model 2016-05-06 10:53:55 +02:00
Samy Pesse 11a0a64d9e Add failing test for #1272 2016-05-06 10:36:55 +02:00
Samy Pesse b6051babac Adapt unit tests for new templating post process 2016-05-05 11:09:35 +02:00
Samy Pesse 34947b5e20 Add mode TemplateOutput to represent {content + blocks} from template 2016-05-05 11:00:05 +02:00
Samy Pesse c621380b66 Add tests for replaceShortcuts 2016-05-04 20:18:48 +02:00
Samy Pesse c4b54033ce Fix #1268: fix shortcuts of template block not being applied 2016-05-04 19:01:31 +02:00
Samy Pesse 4d85d6eb6e Fix #1267: don't fail when "node_modules" folder doesn't exist 2016-05-04 17:41:15 +02:00
Samy Pessé cc9274c9af Bump version to 3.0.0-pre.12 2016-05-04 13:50:52 +02:00
Johan Preynat bbdf7efc34 Fix location.relative(dir, file) trailing slash if file is a directory 2016-05-04 11:51:39 +02:00
Samy Pessé 38a986995a Merge pull request #1264 from GitbookIO/fix/plugin-resources
Fix Loading plugin resources
2016-05-04 10:33:29 +02:00
Johan Preynat ee8d35df7a Update Plugins.listResources() to use the prepared resources list 2016-05-04 10:30:11 +02:00
Johan Preynat be411cd5ff Update copyPluginAssets to use prepared resources 2016-05-04 10:27:21 +02:00
Johan Preynat 2104019171 List resources per plugin in website generator onInit() 2016-05-04 10:26:57 +02:00
Johan Preynat 4e564707df Add resources to website generator state 2016-05-04 10:26:06 +02:00
Johan Preynat da8611e0c3 Add global method resolve(fileName) 2016-05-04 10:25:37 +02:00
Samy Pessé 5d11641a5f Merge pull request #1260 from GitbookIO/move-article
Adds move and remove article modifier.
2016-05-04 09:47:50 +02:00
Samy Pessé 24d38e4fcf Fix #1263: Fix page generation for AsciiDoc 2016-05-04 09:30:09 +02:00
Samy Pessé efb65a8413 Remove filter for pdf/epub/mobi files when outputting 2016-05-03 17:18:13 +02:00
Soreine 3fc90554f5 Add tests, and fixes. 2016-05-03 16:02:58 +02:00
Soreine 4ebe28faae Update insert specification 2016-05-03 13:11:32 +02:00
Soreine 693171cb05 Adds move and remove article modifier. 2016-05-03 12:37:26 +02:00
Samy Pessé 42a2d090aa Bump version to 3.0.0-pre.11 2016-05-03 12:15:30 +02:00
Samy Pessé c84eaa83dd Merge pull request #1259 from GitbookIO/png-data-uri
Adding data-uri support for PNG Images in Books
2016-05-03 12:10:09 +02:00
Johan Preynat 74c3ff80cb lib/models/config.js: Fix wrong call to PluginDependency.listToArray() to PluginDependency.listFromArray() 2016-05-03 12:04:52 +02:00
Johan Preynat 5893d3ee64 lib/parse/parseConfig.js: Remove unused require 2016-05-03 12:04:04 +02:00
Johan Preynat 7f435c56a0 lib/output/modifiers/inlineAssets.js: Add inlinePng() modifier 2016-05-03 11:30:02 +02:00
Johan Preynat 59b7100cf6 lib/output/modifiers/__tests__/inlinePng.js: Add test file for inlinePng modifier 2016-05-03 11:29:40 +02:00
Johan Preynat 0923d1aa24 lib/output/modifiers/inlinePng.js: Add modifier to save inline PNG images 2016-05-03 11:29:17 +02:00
Johan Preynat ffe6c36f2f lib/output/modifiers/resolveImages.js: Don't resolve images when src is a data URI 2016-05-03 11:28:39 +02:00
Johan Preynat 8c4752c5d8 lib/utils/__tests__/location.js: Add location.isDataURI() test file 2016-05-03 11:27:36 +02:00
Johan Preynat d6202e522b lib/utils/images.js: Add convertInlinePNG() method 2016-05-03 11:27:05 +02:00
Johan Preynat 50a24c62c5 lib/utils/location.js: Add isDataURI() method 2016-05-03 11:26:38 +02:00
Soreine 6b17d08892 Adds default file naming utilities in Book model 2016-05-03 11:17:27 +02:00
Samy Pesse 79c91fd8c5 Log npm version being used when running gitbook install 2016-05-02 22:18:19 +02:00
Samy Pesse 95764258ab Update dependencies 2016-05-02 22:16:51 +02:00
Samy Pesse 41e6872557 Merge branch 'fix/plugins_deps' 2016-05-02 22:02:45 +02:00
Samy Pesse 30c96c37c2 Adapt Plugin.listAll to use a list of PluginDependency 2016-05-02 22:02:24 +02:00
Samy Pesse 20f1cf3dff Enable promise long stack trace when testing or debugging 2016-05-02 22:01:05 +02:00
Samy Pesse 8de0d6186e Fix mock fs for windows testing 2016-05-02 19:16:06 +02:00
Samy Pesse edd7d8aefe Update gitbook-asciidoc@1.1.4 2016-05-02 18:45:17 +02:00
Samy Pesse a8da4903f4 Update gitbook-asciidoc@1.1.3 to fix error on linux 2016-05-02 17:27:18 +02:00
Samy Pesse c8df5033f7 Update gitbook-asciidoc@1.1.2 2016-05-02 17:25:41 +02:00
Samy Pesse 412f418da3 Update gitbook-markdown@1.2.3 2016-05-02 17:15:54 +02:00
Johan Preynat 3782d21069 lib/output/ebook/onPage.js: Fix missing page's filepath at Modifiers.inlineAssets() call 2016-05-02 16:54:14 +02:00
Johan Preynat 82a29f4470 lib/models/languages.js: Use lang.ref 2016-05-02 16:37:47 +02:00
Johan Preynat 4a65326b24 Update jest@12.0.2 2016-05-02 16:37:24 +02:00
Johan Preynat d75d8729be lib/output/ebook/onFinish.js: Use lowercase summary.html filename from themes 2016-05-02 16:26:00 +02:00
Samy Pessé c87060454e Add model for plugin dependencies in the book.json 2016-05-02 16:24:20 +02:00
Samy Pessé bbba52c7ff Update gitbook-markdown and gitbook-asciidoc 2016-05-02 16:21:44 +02:00
Samy Pessé 33418bdb33 Document method in utils/command.js 2016-05-02 15:21:08 +02:00
Samy Pessé de47db2b66 Fix escaping of ebook-convert options 2016-05-02 15:09:10 +02:00
Johan Preynat fa6fcffbe1 Improve lib/plugins/__tests__/findInstalled.js 2016-05-02 15:06:02 +02:00
Samy Pessé 93d3f11891 Fix js error when using node < 4.x 2016-05-02 15:03:29 +02:00
Johan Preynat 59479f10fc Fix lib/plugins/__tests__/findInstalled.js 2016-05-02 14:46:54 +02:00
83 changed files with 1503 additions and 382 deletions
+1
View File
@@ -17,6 +17,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Fix `uk` translation
- Fix heading ID including dashes
- Fix error in code highlighting for unknown languages
- Fix data-uri images being handled as external images
- Accept SSH url as plugin version
- Add templating blocks `markdown`, `asciidoc` and `markup`
- Better search experience
+26 -2
View File
@@ -1,15 +1,18 @@
var path = require('path');
var Promise = require('../utils/promise');
var PathUtils = require('../utils/path');
var fs = require('../utils/fs');
var Plugins = require('../plugins');
var deprecate = require('./deprecate');
var encodeConfig = require('./encodeConfig');
var encodeNavigation = require('./encodeNavigation');
var fileToURL = require('../output/helper/fileToURL');
var defaultBlocks = require('../constants/defaultBlocks');
var gitbook = require('../gitbook');
var encodeConfig = require('./encodeConfig');
var encodeSummary = require('./encodeSummary');
var encodeNavigation = require('./encodeNavigation');
/**
Encode a global context into a JS object
It's the context for page's hook, etc
@@ -28,6 +31,7 @@ function encodeGlobal(output) {
var result = {
log: logger,
config: encodeConfig(output, book.getConfig()),
summary: encodeSummary(output, book.getSummary()),
/**
Check if the book is a multilingual book
@@ -67,6 +71,16 @@ function encodeGlobal(output) {
return bookFS.readAsString(fileName);
},
/**
Resolve a file from the book root
@param {String} fileName
@return {String}
*/
resolve: function(fileName) {
return path.resolve(book.getContentRoot(), fileName);
},
template: {
/**
Apply a templating block and returns its result
@@ -96,6 +110,16 @@ function encodeGlobal(output) {
return outputFolder;
},
/**
Resolve a file from the output root
@param {String} fileName
@return {String}
*/
resolve: function(fileName) {
return path.resolve(outputFolder, fileName);
},
/**
Convert a filepath into an url
@return {String}
+51
View File
@@ -0,0 +1,51 @@
var encodeSummaryArticle = require('../json/encodeSummaryArticle');
/**
Encode summary to provide an API to plugin
@param {Output} output
@param {Config} config
@return {Object}
*/
function encodeSummary(output, summary) {
var result = {
/**
Iterate over the summary, it stops when the "iter" returns false
@param {Function} iter
*/
walk: function (iter) {
summary.getArticle(function(article) {
var jsonArticle = encodeSummaryArticle(article, false);
return iter(jsonArticle);
});
},
/**
Get an article by its level
@param {String} level
@return {Object}
*/
getArticleByLevel: function(level) {
var article = summary.getByLevel(level);
return (article? encodeSummaryArticle(article) : undefined);
},
/**
Get an article by its path
@param {String} level
@return {Object}
*/
getArticleByPath: function(level) {
var article = summary.getByPath(level);
return (article? encodeSummaryArticle(article) : undefined);
}
};
return result;
}
module.exports = encodeSummary;
+1 -1
View File
@@ -4,7 +4,7 @@ var options = require('./options');
var initBook = require('../init');
module.exports = {
name: 'install [book]',
name: 'init [book]',
description: 'setup and create files for chapters',
options: [
options.log
+45 -10
View File
@@ -16,12 +16,26 @@ var watch = require('./watch');
var server, lrServer, lrPath;
function waitForCtrlC() {
var d = Promise.defer();
process.on('SIGINT', function() {
d.resolve();
});
return d.promise;
}
function generateBook(args, kwargs) {
var port = kwargs.port;
var outputFolder = getOutputFolder(args);
var book = getBook(args, kwargs);
var Generator = Output.getGenerator(kwargs.format);
var hasWatch = kwargs['watch'];
var hasLiveReloading = kwargs['live'];
// Stop server if running
if (server.isRunning()) console.log('Stopping server');
@@ -29,8 +43,10 @@ function generateBook(args, kwargs) {
.then(function() {
return Parse.parseBook(book)
.then(function(resultBook) {
// Enable livereload plugin
resultBook = ConfigModifier.addPlugin(resultBook, 'livereload');
if (hasLiveReloading) {
// Enable livereload plugin
resultBook = ConfigModifier.addPlugin(resultBook, 'livereload');
}
return Output.generate(Generator, resultBook, {
root: outputFolder
@@ -45,7 +61,7 @@ function generateBook(args, kwargs) {
.then(function() {
console.log('Serving book on http://localhost:'+port);
if (lrPath) {
if (lrPath && hasLiveReloading) {
// trigger livereload
lrServer.changed({
body: {
@@ -55,7 +71,9 @@ function generateBook(args, kwargs) {
}
})
.then(function() {
if (!kwargs.watch) return;
if (!hasWatch) {
return waitForCtrlC();
}
return watch(book.getRoot())
.then(function(filepath) {
@@ -84,7 +102,12 @@ module.exports = {
},
{
name: 'watch',
description: 'Enable/disable file watcher',
description: 'Enable file watcher and live reloading',
defaults: true
},
{
name: 'live',
description: 'Enable live reloading',
defaults: true
},
options.log,
@@ -92,13 +115,25 @@ module.exports = {
],
exec: function(args, kwargs) {
server = new Server();
lrServer = tinylr({});
var hasWatch = kwargs['watch'];
var hasLiveReloading = kwargs['live'];
return Promise.nfcall(lrServer.listen.bind(lrServer), kwargs.lrport)
return Promise()
.then(function() {
if (!hasWatch || !hasLiveReloading) {
return;
}
lrServer = tinylr({});
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('');
});
})
.then(function() {
console.log('Live reload server started on port:', kwargs.lrport);
console.log('Press CTRL+C to quit ...');
console.log('');
return generateBook(args, kwargs);
});
}
+1 -1
View File
@@ -16,7 +16,7 @@ function createMockFS(files) {
var mtime = new Date();
function getFile(filePath) {
var parts = path.normalize(filePath).split('/');
var parts = path.normalize(filePath).split(path.sep);
return parts.reduce(function(list, part, i) {
if (!list) return null;
+2 -1
View File
@@ -36,6 +36,7 @@ module.exports = function createNodeFS(root) {
fsReadFile: fs.readFile,
fsStatFile: fs.stat,
fsReadDir: fsReadDir,
fsLoadObject: fsLoadObject
fsLoadObject: fsLoadObject,
fsReadAsStream: fs.readStream
});
};
+5 -2
View File
@@ -47,8 +47,11 @@ function initBook(rootFolder) {
// Write pages
return Promise.forEach(articles, function(article) {
var filePath = path.join(rootFolder, article.getPath());
if (!filePath) return;
var articlePath = article.getPath();
var filePath = articlePath? path.join(rootFolder, articlePath) : null;
if (!filePath) {
return;
}
return fs.assertFile(filePath, function() {
return fs.ensureFile(filePath)
+1
View File
@@ -20,6 +20,7 @@ function encodeSummaryArticle(article, recursive) {
anchor: article.getAnchor(),
url: article.getUrl(),
path: article.getPath(),
ref: article.getRef(),
articles: articles
};
}
-2
View File
@@ -1,5 +1,3 @@
jest.autoMockOff();
describe('Plugin', function() {
var Plugin = require('../plugin');
+47
View File
@@ -0,0 +1,47 @@
describe('PluginDependency', function() {
var PluginDependency = require('../pluginDependency');
describe('createFromString', function() {
it('must parse name', function() {
var plugin = PluginDependency.createFromString('hello');
expect(plugin.getName()).toBe('hello');
expect(plugin.getVersion()).toBe('*');
});
it('must parse state', function() {
var plugin = PluginDependency.createFromString('-hello');
expect(plugin.getName()).toBe('hello');
expect(plugin.isEnabled()).toBe(false);
});
describe('Version', function() {
it('must parse version', function() {
var plugin = PluginDependency.createFromString('hello@1.0.0');
expect(plugin.getName()).toBe('hello');
expect(plugin.getVersion()).toBe('1.0.0');
});
it('must parse semver', function() {
var plugin = PluginDependency.createFromString('hello@>=4.0.0');
expect(plugin.getName()).toBe('hello');
expect(plugin.getVersion()).toBe('>=4.0.0');
});
});
describe('GIT Version', function() {
it('must handle HTTPS urls', function() {
var plugin = PluginDependency.createFromString('hello@git+https://github.com/GitbookIO/plugin-ga.git');
expect(plugin.getName()).toBe('hello');
expect(plugin.getVersion()).toBe('git+https://github.com/GitbookIO/plugin-ga.git');
});
it('must handle SSH urls', function() {
var plugin = PluginDependency.createFromString('hello@git+ssh://samy@github.com/GitbookIO/plugin-ga.git');
expect(plugin.getName()).toBe('hello');
expect(plugin.getVersion()).toBe('git+ssh://samy@github.com/GitbookIO/plugin-ga.git');
});
});
});
});
+16 -3
View File
@@ -8,11 +8,18 @@ describe('Summary', function() {
articles: [
{
title: 'My First Article',
path: 'README.md'
ref: 'README.md'
},
{
title: 'My Second Article',
path: 'article.md'
ref: 'article.md'
},
{
title: 'Article without ref'
},
{
title: 'Article with absolute ref',
ref: 'https://google.fr'
}
]
},
@@ -33,7 +40,7 @@ describe('Summary', function() {
var part = summary.getByLevel('1');
expect(part).toBeDefined();
expect(part.getArticles().size).toBe(2);
expect(part.getArticles().size).toBe(4);
});
it('can return a Part (2)', function() {
@@ -66,6 +73,12 @@ describe('Summary', function() {
expect(article).toBeDefined();
expect(article.getTitle()).toBe('My Second Article');
});
it('return undefined if not found', function() {
var article = summary.getByPath('NOT_EXISTING.md');
expect(article).toBeFalsy();
});
});
describe('toText', function() {
+84 -8
View File
@@ -8,16 +8,16 @@ describe('TemplateBlock', function() {
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>';
return {
body: '<p>Hello, World!</p>',
parse: true
};
});
// 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
@@ -32,10 +32,77 @@ describe('TemplateBlock', function() {
});
});
describe('getShortcuts', function() {
it('must return undefined if no shortcuts', function() {
var templateBlock = TemplateBlock.create('sayhello', function(block) {
return {
body: '<p>Hello, World!</p>',
parse: true
};
});
expect(templateBlock.getShortcuts()).not.toBeDefined();
});
it('must return complete shortcut', function() {
var templateBlock = TemplateBlock.create('sayhello', {
process: function(block) {
return '<p>Hello, World!</p>';
},
shortcuts: {
parsers: ['markdown'],
start: '$',
end: '-'
}
});
var shortcut = templateBlock.getShortcuts();
expect(shortcut).toBeDefined();
expect(shortcut.getStart()).toEqual('$');
expect(shortcut.getEnd()).toEqual('-');
expect(shortcut.getStartTag()).toEqual('sayhello');
expect(shortcut.getEndTag()).toEqual('endsayhello');
});
});
describe('toNunjucksExt()', function() {
pit('should replace by block anchor', function() {
var templateBlock = TemplateBlock.create('sayhello', function(block) {
return 'Hello';
});
var blocks = {};
// Create a fresh Nunjucks environment
var env = new nunjucks.Environment(null, { autoescape: false });
// Add template block to environement
var Ext = templateBlock.toNunjucksExt({}, blocks);
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) {
blocks = Immutable.fromJS(blocks);
expect(blocks.size).toBe(1);
var blockId = blocks.keySeq().get(0);
var block = blocks.get(blockId);
expect(res).toBe('{{-%' + blockId + '%-}}');
expect(block.get('body')).toBe('Hello');
expect(block.get('name')).toBe('sayhello');
});
});
pit('must create a valid nunjucks extension', function() {
var templateBlock = TemplateBlock.create('sayhello', function(block) {
return '<p>Hello, World!</p>';
return {
body: '<p>Hello, World!</p>',
parse: true
};
});
// Create a fresh Nunjucks environment
@@ -55,7 +122,10 @@ describe('TemplateBlock', function() {
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+'>';
return {
body: '<'+block.kwargs.tag+'>Hello, '+block.kwargs.name+'!</'+block.kwargs.tag+'>',
parse: true
};
});
// Create a fresh Nunjucks environment
@@ -77,7 +147,10 @@ describe('TemplateBlock', function() {
var templateBlock = TemplateBlock.create('sayhello', function(block) {
return Promise()
.then(function() {
return 'Hello ' + block.body;
return {
body: 'Hello ' + block.body,
parse: true
};
});
});
@@ -107,7 +180,10 @@ describe('TemplateBlock', function() {
nested[blk.name] = blk.body.trim();
});
return '<p class="yoda">'+nested.end+' '+nested.start+'</p>';
return {
body: '<p class="yoda">'+nested.end+' '+nested.start+'</p>',
parse: true
};
}
});
+52
View File
@@ -244,6 +244,58 @@ Book.createForFS = function createForFS(fs) {
});
};
/**
Infers the default extension for files
@return {String}
*/
Book.prototype.getDefaultExt = function() {
// Inferring sources
var clues = [
this.getReadme(),
this.getSummary(),
this.getGlossary()
];
// List their extensions
var exts = clues.map(function (clue) {
var file = clue.getFile();
if (file.exists()) {
return file.getParser().getExtensions().first();
} else {
return null;
}
});
// Adds the general default extension
exts.push('.md');
// Choose the first non null
return exts.find(function (e) { return e !== null; });
};
/**
Infer the default path for a Readme.
@return {String}
*/
Book.prototype.getDefaultReadmePath = function() {
return this.getContentRoot()+'README'+this.getDefaultExt();
};
/**
Infer the default path for a Summary.
@return {String}
*/
Book.prototype.getDefaultSummaryPath = function() {
return this.getContentRoot()+'SUMMARY'+this.getDefaultExt();
};
/**
Infer the default path for a Glossary.
@return {String}
*/
Book.prototype.getDefaultGlossaryPath = function() {
return this.getContentRoot()+'GLOSSARY'+this.getDefaultExt();
};
/**
Create a language book from a parent
+28
View File
@@ -2,6 +2,7 @@ var is = require('is');
var Immutable = require('immutable');
var File = require('./file');
var PluginDependency = require('./pluginDependency');
var configDefault = require('../constants/configDefault');
var Config = Immutable.Record({
@@ -52,6 +53,33 @@ Config.prototype.setValue = function(keyPath, value) {
return this.set('values', values);
};
/**
Return a list of plugin dependencies
@return {List<PluginDependency>}
*/
Config.prototype.getPluginDependencies = function() {
var plugins = this.getValue('plugins');
if (is.string(plugins)) {
return PluginDependency.listFromString(plugins);
} else {
return PluginDependency.listFromArray(plugins);
}
};
/**
Update the list of plugins dependencies
@param {List<PluginDependency>}
@return {Config}
*/
Config.prototype.setPluginDependencies = function(deps) {
var plugins = PluginDependency.listFromArray(deps);
return this.setValue('plugins', plugins);
};
/**
Create a new config for a file
+28 -1
View File
@@ -1,5 +1,6 @@
var path = require('path');
var Immutable = require('immutable');
var stream = require('stream');
var File = require('./file');
var Promise = require('../utils/promise');
@@ -13,7 +14,9 @@ var FS = Immutable.Record({
fsReadFile: Function(),
fsStatFile: Function(),
fsReadDir: Function(),
fsLoadObject: null
fsLoadObject: null,
fsReadAsStream: null
});
/**
@@ -111,6 +114,30 @@ FS.prototype.readAsString = function(filename, encoding) {
});
};
/**
Read file as a stream
@param {String} filename
@return {Promise<Stream>}
*/
FS.prototype.readAsStream = function(filename) {
var that = this;
var filepath = that.resolve(filename);
var fsReadAsStream = this.get('fsReadAsStream');
if (fsReadAsStream) {
return Promise(fsReadAsStream(filepath));
}
return this.read(filename)
.then(function(buf) {
var bufferStream = new stream.PassThrough();
bufferStream.end(buf);
return bufferStream;
});
};
/**
Read stat infos about a file
+1 -1
View File
@@ -57,7 +57,7 @@ Languages.createFromList = function(file, langs) {
langs.forEach(function(lang) {
lang = Language({
title: lang.title,
path: lang.path
path: lang.ref
});
list = list.set(lang.getID(), lang);
});
+4
View File
@@ -43,6 +43,10 @@ Parser.prototype.parseGlossary = function(content) {
Parser.prototype.preparePage = function(content) {
var page = this.get('page');
if (!page.prepare) {
return Promise(content);
}
return Promise(page.prepare(content));
};
+13
View File
@@ -139,6 +139,19 @@ Plugin.createFromString = function(s) {
});
};
/**
Create a plugin from a dependency
@param {PluginDependency}
@return {Plugin}
*/
Plugin.createFromDep = function(dep) {
return new Plugin({
name: dep.getName(),
version: dep.getVersion()
});
};
/**
Return NPM id for a plugin name
+115
View File
@@ -0,0 +1,115 @@
var is = require('is');
var Immutable = require('immutable');
var DEFAULT_VERSION = '*';
/*
PluginDependency represents the informations about a plugin
stored in config.plugins
*/
var PluginDependency = Immutable.Record({
name: String(),
// Requirement version (ex: ">1.0.0")
version: String(DEFAULT_VERSION),
// Is this plugin enabled or disabled?
enabled: Boolean(true)
}, 'PluginDependency');
PluginDependency.prototype.getName = function() {
return this.get('name');
};
PluginDependency.prototype.getVersion = function() {
return this.get('version');
};
PluginDependency.prototype.isEnabled = function() {
return this.get('enabled');
};
/**
Create a plugin from a string
@param {String}
@return {Plugin|undefined}
*/
PluginDependency.createFromString = function(s) {
var parts = s.split('@');
var name = parts[0];
var version = parts.slice(1).join('@');
var enabled = true;
if (name[0] === '-') {
enabled = false;
name = name.slice(1);
}
return new PluginDependency({
name: name,
version: version || DEFAULT_VERSION,
enabled: enabled
});
};
/**
Create a PluginDependency from a string
@param {String}
@return {List<PluginDependency>}
*/
PluginDependency.listFromString = function(s) {
var parts = s.split(',');
return PluginDependency.listFromArray(parts);
};
/**
Create a PluginDependency from an array
@param {Array}
@return {List<PluginDependency>}
*/
PluginDependency.listFromArray = function(arr) {
return Immutable.List(arr)
.map(function(entry) {
if (is.string(entry)) {
return PluginDependency.createFromString(entry);
} else {
return PluginDependency({
name: entry.name,
version: entry.version
});
}
})
.filter(function(dep) {
return Boolean(dep.getName());
});
};
/**
Export plugin dependencies as an array
@param {List<PluginDependency>}
@return {Array<String>}
*/
PluginDependency.listToArray = function(arr) {
return arr
.map(function(dep) {
var result;
if (dep.isEnabled()) {
result += '-';
}
result += dep.getName();
if (dep.getVersion() !== DEFAULT_VERSION) {
result += '@' + dep.getVersion();
}
return result;
})
.toJS();
};
module.exports = PluginDependency;
+39 -2
View File
@@ -56,7 +56,7 @@ Summary.prototype.getArticle = function(iter, partIter) {
Return a part/article by its level
@param {String} level
@return {Article}
@return {Article|Part}
*/
Summary.prototype.getByLevel = function(level) {
function iterByLevel(article) {
@@ -74,7 +74,12 @@ Summary.prototype.getByLevel = function(level) {
*/
Summary.prototype.getByPath = function(filePath) {
return this.getArticle(function(article) {
return (LocationUtils.areIdenticalPaths(article.getPath(), filePath));
var articlePath = article.getPath();
return (
articlePath &&
LocationUtils.areIdenticalPaths(articlePath, filePath)
);
});
};
@@ -129,6 +134,27 @@ Summary.prototype.getPrevArticle = function(current) {
return prev;
};
/**
Return the parent article, or parent part of an article
@param {String|Article} current
@return {Article|Part|Null}
*/
Summary.prototype.getParent = function (level) {
// Coerce to level
level = is.string(level)? level : level.getLevel();
// Get parent level
var parentLevel = getParentLevel(level);
if (!parentLevel) {
return null;
}
// Get parent of the position
var parentArticle = this.getByLevel(parentLevel);
return parentArticle || null;
};
/**
Render summary as text
@@ -188,4 +214,15 @@ Summary.createFromParts = function createFromParts(file, parts) {
});
};
/**
Returns parent level of a level
@param {String} level
@return {String}
*/
function getParentLevel(level) {
var parts = level.split('.');
return parts.slice(0, -1).join('.');
}
module.exports = Summary;
+3 -2
View File
@@ -30,12 +30,13 @@ SummaryArticle.prototype.getArticles = function() {
};
/**
Return how deep the article is
Return how deep the article is.
The README has a depth of 1
@return {Number}
*/
SummaryArticle.prototype.getDepth = function() {
return this.getLevel().split('.').length;
return (this.getLevel().split('.').length - 1);
};
/**
+31 -53
View File
@@ -4,11 +4,10 @@ var Immutable = require('immutable');
var Promise = require('../utils/promise');
var genKey = require('../utils/genKey');
var TemplateShortcut = require('./templateShortcut');
var NODE_ENDARGS = '%%endargs%%';
var blockBodies = {};
var TemplateBlock = Immutable.Record({
// Name of block, also the start tag
name: String(),
@@ -23,26 +22,13 @@ var TemplateBlock = Immutable.Record({
blocks: Immutable.List(),
// List of shortcuts to replace with this block
shortcuts: Immutable.List(),
// Function to execute in post processing
post: null,
parse: true
shortcuts: Immutable.Map()
}, '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());
};
@@ -55,8 +41,19 @@ TemplateBlock.prototype.getBlocks = function() {
return this.get('blocks');
};
/**
Return shortcuts associated with this block or undefined
@return {TemplateShortcut|undefined}
*/
TemplateBlock.prototype.getShortcuts = function() {
return this.get('shortcuts');
var shortcuts = this.get('shortcuts');
if (shortcuts.size === 0) {
return undefined;
}
return TemplateShortcut.createForBlock(this, shortcuts);
};
/**
@@ -73,7 +70,9 @@ TemplateBlock.prototype.getExtensionName = function() {
@return {Nunjucks.Extension}
*/
TemplateBlock.prototype.toNunjucksExt = function(mainContext) {
TemplateBlock.prototype.toNunjucksExt = function(mainContext, blocksOutput) {
blocksOutput = blocksOutput || {};
var that = this;
var name = this.getName();
var endTag = this.getEndTag();
@@ -183,7 +182,7 @@ TemplateBlock.prototype.toNunjucksExt = function(mainContext) {
return that.applyBlock(mainBlock, ctx);
})
.then(function(result) {
return that.blockResultToHtml(result);
return that.blockResultToHtml(result, blocksOutput);
})
.nodeify(callback);
};
@@ -209,19 +208,19 @@ TemplateBlock.prototype.applyBlock = function(inner, context) {
var r = processFn.call(context, inner);
if (Promise.isPromiseAlike(r)) {
return r.then(this.handleBlockResult.bind(this));
return r.then(this.normalizeBlockResult.bind(this));
} else {
return this.handleBlockResult(r);
return this.normalizeBlockResult(r);
}
};
/**
Handle result from a block process function
Normalize result from a block process function
@param {Object} result
@param {Object|String} result
@return {Object}
*/
TemplateBlock.prototype.handleBlockResult = function(result) {
TemplateBlock.prototype.normalizeBlockResult = function(result) {
if (is.string(result)) {
result = { body: result };
}
@@ -234,19 +233,20 @@ TemplateBlock.prototype.handleBlockResult = function(result) {
Convert a block result to HTML
@param {Object} result
@param {Object} blocksOutput: stored post processing blocks in this object
@return {String}
*/
TemplateBlock.prototype.blockResultToHtml = function(result) {
var parse = this.getParse();
TemplateBlock.prototype.blockResultToHtml = function(result, blocksOutput) {
var indexedKey;
var toIndex = (!parse) || (this.getPost() !== undefined);
var toIndex = (!result.parse) || (result.post !== undefined);
if (toIndex) {
indexedKey = TemplateBlock.indexBlockResult(result);
indexedKey = genKey();
blocksOutput[indexedKey] = result;
}
// Parsable block, just return it
if (parse) {
if (result.parse) {
return result.body;
}
@@ -255,29 +255,6 @@ TemplateBlock.prototype.blockResultToHtml = function(result) {
};
/**
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
@@ -292,8 +269,9 @@ TemplateBlock.create = function(blockName, block) {
});
}
block = new TemplateBlock(block);
block = block.set('name', blockName);
return new TemplateBlock(block);
return block;
};
/**
+2 -2
View File
@@ -67,7 +67,7 @@ TemplateEngine.prototype.getBlock = function(name) {
@return {Nunjucks.Environment}
*/
TemplateEngine.prototype.toNunjucks = function() {
TemplateEngine.prototype.toNunjucks = function(blocksOutput) {
var loader = this.getLoader();
var blocks = this.getBlocks();
var filters = this.getFilters();
@@ -101,7 +101,7 @@ TemplateEngine.prototype.toNunjucks = function() {
// Add blocks
blocks.forEach(function(block) {
var extName = block.getExtensionName();
var Ext = block.toNunjucksExt(context);
var Ext = block.toNunjucksExt(context, blocksOutput);
env.addExtension(extName, new Ext());
});
+44
View File
@@ -0,0 +1,44 @@
var Immutable = require('immutable');
var TemplateOutput = Immutable.Record({
// Text content of the template
content: String(),
// Map of blocks to replace / post process
blocks: Immutable.Map()
}, 'TemplateOutput');
TemplateOutput.prototype.getContent = function() {
return this.get('content');
};
TemplateOutput.prototype.getBlocks = function() {
return this.get('blocks');
};
/**
Update content of this output
@param {String} content
@return {TemplateContent}
*/
TemplateOutput.prototype.setContent = function(content) {
return this.set('content', content);
};
/**
Create a TemplateOutput from a text content
and an object containing block definition
@param {String} content
@param {Object} blocks
@return {TemplateOutput}
*/
TemplateOutput.create = function(content, blocks) {
return new TemplateOutput({
content: content,
blocks: Immutable.fromJS(blocks)
});
};
module.exports = TemplateOutput;
+73
View File
@@ -0,0 +1,73 @@
var Immutable = require('immutable');
var is = require('is');
/*
A TemplateShortcut is defined in plugin's template blocks
to replace content with a templating block using delimiters.
*/
var TemplateShortcut = Immutable.Record({
// List of parser names accepting this shortcut
parsers: Immutable.Map(),
start: String(),
end: String(),
startTag: String(),
endTag: String()
}, 'TemplateShortcut');
TemplateShortcut.prototype.getStart = function() {
return this.get('start');
};
TemplateShortcut.prototype.getEnd = function() {
return this.get('end');
};
TemplateShortcut.prototype.getStartTag = function() {
return this.get('startTag');
};
TemplateShortcut.prototype.getEndTag = function() {
return this.get('endTag');
};
TemplateShortcut.prototype.getParsers = function() {
return this.get('parsers');
};
/**
Test if this shortcut accept a parser
@param {Parser|String} parser
@return {Boolean}
*/
TemplateShortcut.prototype.acceptParser = function(parser) {
if (!is.string(parser)) {
parser = parser.getName();
}
var parserNames = this.get('parsers');
return parserNames.includes(parser);
};
/**
Create a shortcut for a block
@param {TemplateBlock} block
@param {Map} details
@return {TemplateShortcut}
*/
TemplateShortcut.createForBlock = function(block, details) {
details = Immutable.fromJS(details);
return new TemplateShortcut({
parsers: details.get('parsers'),
start: details.get('start'),
end: details.get('end'),
startTag: block.getName(),
endTag: block.getEndTag()
});
};
module.exports = TemplateShortcut;
+9 -4
View File
@@ -1,4 +1,4 @@
var PluginDependency = require('../../models/pluginDependency');
/**
Add a plugin to a book's configuration
@@ -9,10 +9,15 @@
*/
function addPlugin(book, plugin, version) {
var config = book.getConfig();
var plugins = config.getValue('plugins', []);
var deps = config.getPluginDependencies();
plugins = plugins.push('livereload');
config = config.setValue('plugins', plugins);
var dep = PluginDependency({
name: plugin,
version: version
});
deps = deps.push(dep);
config = config.setPluginDependencies(deps);
return book.setConfig(config);
}
+5 -7
View File
@@ -8,15 +8,13 @@
*/
function removePlugin(book, pluginName) {
var config = book.getConfig();
var plugins = config.getValue('plugins', []);
var deps = config.getPluginDependencies();
// Find index of this plugin
var index = plugins.findIndex(function(plugin) {
return plugin === pluginName;
deps = deps.filter(function(dep) {
return dep.getName() === pluginName;
});
plugins = plugins.delete(index);
config = config.setValue('plugins', plugins);
config = config.setPluginDependencies(deps);
return book.setConfig(config);
}
@@ -0,0 +1,45 @@
var Immutable = require('immutable');
var Summary = require('../../../models/summary');
var File = require('../../../models/file');
describe('mergeAtLevel', function() {
var mergeAtLevel = require('../mergeAtLevel');
var summary = Summary.createFromParts(File(), [
{
articles: [
{
title: '1.1',
path: '1.1'
},
{
title: '1.2',
path: '1.2'
}
]
},
{
title: 'Part I',
articles: []
}
]);
it('should edit a part', function() {
var beforeChildren = summary.getByLevel('1').getArticles();
var newSummary = mergeAtLevel(summary, '1', {title: 'Part O'});
var edited = newSummary.getByLevel('1');
expect(edited.getTitle()).toBe('Part O');
// Same children
expect(Immutable.is(beforeChildren, edited.getArticles())).toBe(true);
});
it('should edit a part', function() {
var beforePath = summary.getByLevel('1.2').getPath();
var newSummary = mergeAtLevel(summary, '1.2', {title: 'Renamed article'});
var edited = newSummary.getByLevel('1.2');
expect(edited.getTitle()).toBe('Renamed article');
// Same children
expect(Immutable.is(beforePath, edited.getPath())).toBe(true);
});
});
@@ -0,0 +1,68 @@
var Immutable = require('immutable');
var Summary = require('../../../models/summary');
var File = require('../../../models/file');
describe('moveArticle', function() {
var moveArticle = require('../moveArticle');
var summary = Summary.createFromParts(File(), [
{
articles: [
{
title: '1.1',
path: '1.1'
},
{
title: '1.2',
path: '1.2'
}
]
},
{
title: 'Part I',
articles: [
{
title: '2.1',
path: '2.1',
articles: [
{
title: '2.1.1',
path: '2.1.1'
},
{
title: '2.1.2',
path: '2.1.2'
}
]
},
{
title: '2.2',
path: '2.2'
}
]
}
]);
it('should move an article at in place', function() {
var newSummary = moveArticle(summary, '2.1', '2.1');
expect(Immutable.is(summary, newSummary)).toBe(true);
});
it('should move an article to an previous level', function() {
var newSummary = moveArticle(summary, '2.2', '2.1');
var moved = newSummary.getByLevel('2.1');
var other = newSummary.getByLevel('2.2');
expect(moved.getTitle()).toBe('2.2');
expect(other.getTitle()).toBe('2.1');
});
it('should move an article to a next level', function() {
var newSummary = moveArticle(summary, '2.1', '2.2');
var moved = newSummary.getByLevel('2.1');
var other = newSummary.getByLevel('2.2');
expect(moved.getTitle()).toBe('2.2');
expect(other.getTitle()).toBe('2.1');
});
});
+2 -2
View File
@@ -1,4 +1,4 @@
var editArticle = require('./editArticle');
var mergeAtLevel = require('./mergeAtLevel');
/**
Edit title of an article
@@ -9,7 +9,7 @@ var editArticle = require('./editArticle');
@return {Summary}
*/
function editArticleTitle(summary, level, newTitle) {
return editArticle(summary, level, {
return mergeAtLevel(summary, level, {
title: newTitle
});
}
-1
View File
@@ -1,4 +1,3 @@
/**
Edit title of a part in the summary
+2 -1
View File
@@ -1,6 +1,7 @@
module.exports = {
insertArticle: require('./insertArticle'),
moveArticle: require('./moveArticle'),
removeArticle: require('./removeArticle'),
unshiftArticle: require('./unshiftArticle'),
editPartTitle: require('./editPartTitle'),
+21 -35
View File
@@ -1,25 +1,14 @@
var is = require('is');
var SummaryArticle = require('../../models/summaryArticle');
var editArticle = require('./editArticle');
var mergeAtLevel = require('./mergeAtLevel');
var indexArticleLevels = require('./indexArticleLevels');
/**
Get level of parent of an article
@param {String} level
@return {String}
*/
function getParentLevel(level) {
var parts = level.split('.');
return parts.slice(0, -1).join('.');
}
/**
Insert an article in a summary at a specific position
Returns a new Summary with the article at the given level, with
subsequent article shifted.
@param {Summary} summary
@param {String|Article} level: level to insert after
@param {String|Article} level: level to insert at
@param {Article} article
@return {Summary}
*/
@@ -27,37 +16,34 @@ function insertArticle(summary, level, article) {
article = SummaryArticle(article);
level = is.string(level)? level : level.getLevel();
var parentLevel = getParentLevel(level);
if (!parentLevel) {
// todo: insert new part
return summary;
}
// Get parent of the position
var parentArticle = summary.getByLevel(parentLevel);
if (!parentLevel) {
var parent = summary.getParent(level);
if (!parent) {
return summary;
}
// Find the index to insert at
var articles = parentArticle.getArticles();
var index = articles.findIndex(function(art) {
return art.getLevel() === level;
});
if (!index) {
return summary;
}
var articles = parent.getArticles();
var index = getLeafIndex(level);
// Insert the article at the right index
articles = articles.insert(index, article);
// Reindex the level from here
parentArticle = parentArticle.set('articles', articles);
parentArticle = indexArticleLevels(parentArticle);
parent = parent.set('articles', articles);
parent = indexArticleLevels(parent);
return editArticle(summary, parentLevel, parentArticle);
return mergeAtLevel(summary, parent.getLevel(), parent);
}
/**
@param {String}
@return {Number} The index of this level within its parent's children
*/
function getLeafIndex(level) {
var arr = level.split('.').map(function (char) {
return parseInt(char, 10);
});
return arr[arr.length - 1] - 1;
}
module.exports = insertArticle;
@@ -11,16 +11,17 @@ function editArticleInList(articles, level, newArticle) {
return articles.map(function(article) {
var articleLevel = article.getLevel();
if (articleLevel == level) {
if (articleLevel === level) {
// it is the article to edit
return article.merge(newArticle);
}
if (level.indexOf(articleLevel) === 0) {
} else if (level.indexOf(articleLevel) === 0) {
// it is a parent
var articles = editArticleInList(article.getArticles(), level, newArticle);
return article.set('articles', articles);
} else {
// This is not the article you are looking for
return article;
}
return article;
});
}
@@ -35,36 +36,40 @@ function editArticleInList(articles, level, newArticle) {
*/
function editArticleInPart(part, level, newArticle) {
var articles = part.getArticles();
articles = editArticleInList(articles);
articles = editArticleInList(articles, level, newArticle);
return part.set('articles', articles);
}
/**
Edit an article in a summary
Edit an article, or a part, in a summary. Does a shallow merge.
@param {Summary} summary
@param {String} level
@param {Article} newArticle
@param {Article|Part} newValue
@return {Summary}
*/
function editArticle(summary, level, newArticle) {
var parts = summary.getParts();
function mergeAtLevel(summary, level, newValue) {
var levelParts = level.split('.');
var partIndex = Number(levelParts[0]);
var partIndex = Number(levelParts[0]) -1;
var parts = summary.getParts();
var part = parts.get(partIndex);
if (!part) {
return summary;
}
part = editArticleInPart(part, level, newArticle);
parts = parts.set(partIndex, part);
var isEditingPart = levelParts.length < 2;
if (isEditingPart) {
part = part.merge(newValue);
} else {
part = editArticleInPart(part, level, newValue);
}
parts = parts.set(partIndex, part);
return summary.set('parts', parts);
}
module.exports = editArticle;
module.exports = mergeAtLevel;
+82
View File
@@ -0,0 +1,82 @@
var is = require('is');
var removeArticle = require('./removeArticle');
var insertArticle = require('./insertArticle');
/**
Returns a new summary, with the given article removed from its
origin level, and placed at the given target level.
@param {Summary} summary
@param {String|SummaryArticle} origin: level to remove
@param {String|SummaryArticle} target: the level where the article will be found
@return {Summary}
*/
function moveArticle(summary, origin, target) {
// Coerce to level
var originLevel = is.string(origin)? origin : origin.getLevel();
var targetLevel = is.string(target)? target : target.getLevel();
var article = summary.getByLevel(originLevel);
// Remove
var removed = removeArticle(summary, origin);
// Adjust targetLevel if removing impacted it
targetLevel = arrayToLevel(
shiftLevel(levelToArray(originLevel),
levelToArray(targetLevel)));
// Re-insert
return insertArticle(removed, target, article);
}
/**
@param {Array<Number>} removedLevel
@param {Array<Number>} level The level to udpate
@return {Array<Number>}
*/
function shiftLevel(removedLevel, level) {
if (level.length === 0) {
// `removedLevel` is under level, so no effect
return level;
} else if (removedLevel.length === 0) {
// Either `level` is a child of `removedLevel`... or they are equal
// This is undefined behavior.
return level;
}
var removedRoot = removedLevel[0];
var root = level[0];
var removedRest = removedLevel.slice(1);
var rest = level.slice(1);
if (removedRoot < root) {
// It will shift levels at this point. The rest is unchanged.
return Array.prototype.concat(root - 1, rest);
} else if (removedRoot === root) {
// Look deeper
return Array.prototype.concat(root, shiftLevel(removedRest, rest));
} else {
// No impact
return level;
}
}
/**
@param {String}
@return {Array<Number>}
*/
function levelToArray(l) {
return l.split('.').map(function (char) {
return parseInt(char, 10);
});
}
/**
@param {Array<Number>}
@return {String}
*/
function arrayToLevel(a) {
return a.join('.');
}
module.exports = moveArticle;
+37
View File
@@ -0,0 +1,37 @@
var is = require('is');
var mergeAtLevel = require('./mergeAtLevel');
var indexArticleLevels = require('./indexArticleLevels');
/**
Remove an article from a level.
@param {Summary} summary
@param {String|SummaryArticle} level: level to remove
@return {Summary}
*/
function removeArticle(summary, level) {
// Coerce to level
level = is.string(level)? level : level.getLevel();
var parent = summary.getParent(level);
var articles = parent.getArticles();
// Find the index to remove
var index = articles.findIndex(function(art) {
return art.getLevel() === level;
});
if (index === -1) {
return summary;
}
// Remove from children
articles = articles.remove(index);
parent = parent.set('articles', articles);
// Reindex the level from here
parent = indexArticleLevels(parent);
return mergeAtLevel(summary, parent.getLevel(), parent);
}
module.exports = removeArticle;
+1 -1
View File
@@ -4,7 +4,7 @@ var SummaryPart = require('../../models/summaryPart');
var indexLevels = require('./indexLevels');
/**
Insert an article at the
Insert an article at the beginning of summary
@param {Summary} summary
@param {Article} article
+24
View File
@@ -12,6 +12,30 @@ describe('WebsiteGenerator', function() {
});
});
pit('should copy asset files', function() {
return generateMock(WebsiteGenerator, {
'README.md': 'Hello World',
'myJsFile.js': 'var a = "test";',
'folder': {
'AnotherAssetFile.md': '# Even md'
}
})
.then(function(folder) {
expect(folder).toHaveFile('index.html');
expect(folder).toHaveFile('myJsFile.js');
expect(folder).toHaveFile('folder/AnotherAssetFile.md');
});
});
pit('should generate an index.html for AsciiDoc', function() {
return generateMock(WebsiteGenerator, {
'README.adoc': 'Hello World'
})
.then(function(folder) {
expect(folder).toHaveFile('index.html');
});
});
pit('should generate an HTML file for each articles', function() {
return generateMock(WebsiteGenerator, {
'README.md': 'Hello World',
+3 -4
View File
@@ -22,16 +22,15 @@ function getPDFTemplate(output, type) {
var context = JSONUtils.encodeOutput(output);
context.page = {
num: '_PAGENUM_',
title: '_TITLE_',
section: '_SECTION_'
title: '_SECTION_'
};
// Render the theme
return Templating.renderFile(engine, 'ebook/' + filePath, context)
// Inline css and assets
.then(function(html) {
return Promise.nfcall(juice.juiceResources, html, {
.then(function(tplOut) {
return Promise.nfcall(juice.juiceResources, tplOut.getContent(), {
webResources: {
relativeTo: outputRoot
}
+6 -5
View File
@@ -9,6 +9,7 @@ var command = require('../../utils/command');
var writeFile = require('../helper/writeFile');
var getConvertOptions = require('./getConvertOptions');
var SUMMARY_FILE = 'SUMMARY.html';
/**
Write the SUMMARY.html
@@ -20,16 +21,16 @@ function writeSummary(output) {
var options = output.getOptions();
var prefix = options.get('prefix');
var filePath = 'SUMMARY.html';
var filePath = SUMMARY_FILE;
var engine = WebsiteGenerator.createTemplateEngine(output, filePath);
var context = JSONUtils.encodeOutput(output);
// Render the theme
return Templating.renderFile(engine, prefix + '/SUMMARY.html', context)
return Templating.renderFile(engine, prefix + '/summary.html', context)
// Write it to the disk
.then(function(html) {
return writeFile(output, filePath, html);
.then(function(tplOut) {
return writeFile(output, filePath, tplOut.getContent());
});
}
@@ -53,7 +54,7 @@ function runEbookConvert(output) {
.then(function(options) {
var cmd = [
'ebook-convert',
path.resolve(outputFolder, 'SUMMARY.html'),
path.resolve(outputFolder, SUMMARY_FILE),
path.resolve(outputFolder, 'index.' + format),
command.optionsToShellArgs(options)
].join(' ');
+1 -1
View File
@@ -12,7 +12,7 @@ function onPage(output, page) {
// Inline assets
return Modifiers.modifyHTML(page, [
Modifiers.inlineAssets(options.get('root'))
Modifiers.inlineAssets(options.get('root'), page.getFile().getPath())
])
// Write page using website generator
+10 -4
View File
@@ -47,12 +47,18 @@ function generatePage(output, page) {
return Templating.render(engine, filePath, content, context);
})
// Render page using parser (markdown -> HTML)
.then(parser.parsePage.bind(parser)).get('content')
.then(function(output) {
var content = output.getContent();
return parser.parsePage(content)
.then(function(result) {
return output.setContent(result.content);
});
})
// Post processing for templating syntax
.then(function(content) {
return Templating.postRender(engine, content);
.then(function(output) {
return Templating.postRender(engine, output);
})
// Return new page
@@ -0,0 +1,25 @@
var cheerio = require('cheerio');
var tmp = require('tmp');
var inlinePng = require('../inlinePng');
describe('inlinePng', function() {
var dir;
beforeEach(function() {
dir = tmp.dirSync();
});
pit('should write an inline PNG using data URI as a file', function() {
var $ = cheerio.load('<img alt="GitBook Logo 20x20" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUEAYAAADdGcFOAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAAsTAAALEwEAmpwYAAABWWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgpMwidZAAAF+klEQVRIDY3Wf5CVVR3H8c9z791fyI9dQwdQ4TTI7wEWnQZZAa/mJE4Z0OaKUuN1KoaykZxUGGHay+iIVFMoEYrUPhDCKEKW2ChT8dA0RCSxWi6EW3sYYpcfxq5C+4O9957O+7m7O/qHQ9/XzH1+nHuec57z8wkWTsKw0y6N/LxXN6KzTnEUHi8eP/l3YStSU/MdsYvBbGh8six2YXcbcgc++QkfTQkWz/81KtqDA0hlUoWnsX+5uxe5X365BB9my2bjrHNHccLk16BpS9CExjcmXMDbD6wehdyEjxbjz1uK1zn9qga6dcfnMLXeXY/qjuQqTF4W1MKke8ZgeNhjMCxMPIWSd4OF78C55CFI/1kF6WwXpMqjkAZ/CKniNDrCsmU4lE1YbPlgR2x7R39FF23D4mq3A1+Z35PGTNs1E1XhxcGQOh6HNPwXkK56BVJhOaRg/pvoHXNxHFw410B25EYE2RMvI0i/twFJvXcrFObykEa+DmnQGLwYqR0l2a6JqItaj8C/4E2QxtZCofkC8tF1t8HZc/fAZaLnIF2xEsoEtW1w7vBSSFtfhDTnCki9cSi81Ain1uko2Ld+Dmf2rkUq0/5t+PYbFtPQdkjzNiAXTWtDEF49FgkzJInAVPwNyhzcDOmrdZCm/Rn+ebWtcPs+/U24hmg2XL0rRkPPELh9R8fDtXR2oC/VuZbGaci79Ajkb6lZgfyYtyzy/X9s6T/pO/ZfN/RdNxxIwTWM2wbX8KVmuIaEqmKm6zEondwGpd0SyOy5DrJ//TFkX9kMhd3XQHbEVCSsm4OECV5HIv2p15CwfWPSntoHRbv2Q1HzSvSlSqZwATIuBxk/zZBOBbdB+u9hSKU3Q7pwAjInZkFm6U8hu7MSMqe/Dqn8fUj5GVCmpxK+4N/F1LMa0p5eSOPqIPP7NGSunAI/+R6GnzQzIBt8A1LC/QZ+6HwLst1rITv0n5CtXgSZ78yFTNkR+FdeDZneJkip3fAtsQ5Scilkek7CH9dAmjIWvkK7IXXOh6/IzZDNPQdZXR1TQmdjKv0ZfEu0YKDpNflpyG5aDtnRv8VAuu3dBV+huyBbvgdS97tQNLQc0mfugKy5Cb4BipPIXvsUpK5N8Mvao/Bd3QDZRH9Rrtj3Cl6FHwPFMLmNkKrj8BnHoT+XX6f2wl+XxFS4Ab7C72Dgf7bi+5DpTkNm8kQMpCs/BzIlz8LfPxnzLdh3EjwMX4GX4Ju4GNb9A1L7k/D3J8b6kv2LFCtmCmcgUzoJsr2z4MfwFsh87xikZefg188fYaAhpPUxm3ge/vFnYkoED0HqeQiyJYcwkNGWnoNv6s9C1p1Bf/389VYoCjohW7UfMms3wXdpBv7+FEiPLIHs4DIMNERUNhbSpY3wk6QOsqlCDVx2xCrInMpBmfNPQOnzKxBkkrugdOl9GKigSZZCUWIm/GqwDtLUI5D+WAOlb9wKP0YvQLbjZSjsaYaL/n0/FA3fDtnCGihK5UYjCK+ZDr+TDIKLdm2Fs1UOzo76F5wO74XSZj0S6d7RCMLkCshcXALZxaWQRjXDZQ62oRAdCeG/Ju5HELX2QFH3C0hkRy6GovyfwF58AoVbguOxyB2H7/I34Gf11yANnQSp7Vr4MbQH0vg7kbNNp5AM3UrIVDchnz56B1Jm573wW9gZSFVPwO/hefg5FsIvN09CchtQCIOFw/F5U8ii3CZn4cqo7C8YlXEPYkx9cacZl00+iwnprrtwVdj1Q/gXmAs/pu6LZc9XQOGgSvh19n2cDZN341g2EcfxTEGwH/RewqlMsUfbbWIGLjUG+j/j9nokD1beiOvLS5dhjr30Gu6ZnivgdtM/6VJvY1+6pBHbH+h9CX84vfMxNJtisYVFlys+WNCIZJNmIsjohlhNSQC3f8R55H+y/hjkN8GPR9ndCLJxT4/3n0Px51ay8XQnNrYfDJHf//Fc0oMrEZSeeQGJ7+Z+gKCgLbHNWgXnB9FlYt5JaN38JIINC95EakjtAqQeuUx21c5B6tEFf0fSfbEFQf28Z6D6y+X/H0jf40QQJhYwAAAAAElFTkSuQmCC"/>');
return inlinePng(dir.name, 'index.html', $)
.then(function() {
var $img = $('img');
var src = $img.attr('src');
expect(dir.name).toHaveFile(src);
});
});
});
+3 -1
View File
@@ -1,4 +1,6 @@
var is = require('is');
var Immutable = require('immutable');
var Promise = require('../../utils/promise');
var editHTMLElement = require('./editHTMLElement');
@@ -9,7 +11,7 @@ var editHTMLElement = require('./editHTMLElement');
@return {String}
*/
function getLanguageForClass(classNames) {
return classNames
return Immutable.List(classNames)
.map(function(cl) {
// Markdown
if (cl.search('lang-') === 0) {
+3 -1
View File
@@ -1,5 +1,6 @@
var svgToImg = require('./svgToImg');
var svgToPng = require('./svgToPng');
var inlinePng = require('./inlinePng');
var resolveImages = require('./resolveImages');
var fetchRemoteImages = require('./fetchRemoteImages');
@@ -20,7 +21,8 @@ function inlineAssets(rootFolder, currentFile) {
.then(fetchRemoteImages.bind(null, rootFolder, currentFile, $))
.then(svgToImg.bind(null, rootFolder, currentFile, $))
.then(svgToPng.bind(null, rootFolder, currentFile, $));
.then(svgToPng.bind(null, rootFolder, currentFile, $))
.then(inlinePng.bind(null, rootFolder, currentFile, $));
};
}
+47
View File
@@ -0,0 +1,47 @@
var crc = require('crc');
var path = require('path');
var imagesUtil = require('../../utils/images');
var fs = require('../../utils/fs');
var LocationUtils = require('../../utils/location');
var editHTMLElement = require('./editHTMLElement');
/**
Convert all inline PNG images to PNG file
@param {String} rootFolder
@param {HTMLDom} $
@return {Promise}
*/
function inlinePng(rootFolder, currentFile, $) {
var currentDirectory = path.dirname(currentFile);
return editHTMLElement($, 'img', function($img) {
var src = $img.attr('src');
if (!LocationUtils.isDataURI(src)) {
return;
}
// We avoid generating twice the same PNG
var hash = crc.crc32(src).toString(16);
var fileName = hash + '.png';
// Result file path
var filePath = path.join(rootFolder, fileName);
return fs.assertFile(filePath, function() {
return imagesUtil.convertInlinePNG(src, filePath);
})
.then(function() {
// Convert filename to a relative filename
fileName = LocationUtils.relative(currentDirectory, fileName);
// Replace src
$img.attr('src', fileName);
});
});
}
module.exports = inlinePng;
+1 -1
View File
@@ -16,7 +16,7 @@ function resolveImages(currentFile, $) {
return editHTMLElement($, 'img', function($img) {
var src = $img.attr('src');
if (LocationUtils.isExternal(src)) {
if (LocationUtils.isExternal(src) || LocationUtils.isDataURI(src)) {
return;
}
+5
View File
@@ -17,6 +17,11 @@ function resolveLinks(currentFile, resolveFile, $) {
return editHTMLElement($, 'a', function($a) {
var href = $a.attr('href');
// Don't change a tag without href
if (!href) {
return;
}
if (LocationUtils.isExternal(href)) {
$a.attr('_target', 'blank');
return;
+7 -5
View File
@@ -78,14 +78,16 @@ function copyAssets(output, plugin) {
function copyResources(output, plugin) {
var logger = output.getLogger();
var options = output.getOptions();
var prefix = options.get('prefix');
var options = output.getOptions();
var outputRoot = options.get('root');
var pluginRoot = plugin.getPath();
var resources = plugin.getResources(prefix);
var state = output.getState();
var resources = state.getResources();
var assetsFolder = resources.get('assets');
var pluginRoot = plugin.getPath();
var pluginResources = resources.get(plugin.getName());
var assetsFolder = pluginResources.get('assets');
var assetOutputFolder = path.join(outputRoot, 'gitbook', plugin.getNpmID());
if (!assetsFolder) {
+5 -4
View File
@@ -10,16 +10,17 @@ var fs = require('../../utils/fs');
function onAsset(output, asset) {
var book = output.getBook();
var options = output.getOptions();
var bookFS = book.getContentFS();
var rootFolder = book.getContentRoot();
var outputFolder = options.get('root');
var filePath = path.resolve(rootFolder, asset);
var outputPath = path.resolve(outputFolder, asset);
return fs.ensureFile(outputPath)
.then(function() {
return fs.copy(filePath, outputPath);
return bookFS.readAsStream(asset)
.then(function(stream) {
return fs.writeStream(outputPath, stream);
});
})
.thenResolve(output);
}
+2 -2
View File
@@ -27,8 +27,8 @@ function onFinish(output) {
return Templating.renderFile(engine, prefix + '/languages.html', context)
// Write it to the disk
.then(function(html) {
return writeFile(output, filePath, html);
.then(function(tplOut) {
return writeFile(output, filePath, tplOut.getContent());
});
}
+2
View File
@@ -2,6 +2,7 @@ var Promise = require('../../utils/promise');
var copyPluginAssets = require('./copyPluginAssets');
var prepareI18n = require('./prepareI18n');
var prepareResources = require('./prepareResources');
/**
Initialize the generator
@@ -12,6 +13,7 @@ var prepareI18n = require('./prepareI18n');
function onInit(output) {
return Promise(output)
.then(prepareI18n)
.then(prepareResources)
.then(copyPluginAssets);
}
+12 -8
View File
@@ -18,11 +18,15 @@ var fileToOutput = require('../helper/fileToOutput');
@param {Page} page
*/
function onPage(output, page) {
var options = output.getOptions();
var file = page.getFile();
var prefix = options.get('prefix');
var book = output.getBook();
var plugins = output.getPlugins();
var options = output.getOptions();
var prefix = options.get('prefix');
var file = page.getFile();
var book = output.getBook();
var plugins = output.getPlugins();
var state = output.getState();
var resources = state.getResources();
var engine = createTemplateEngine(output, page.getPath());
@@ -38,7 +42,7 @@ function onPage(output, page) {
// Generate the context
var context = JSONUtils.encodeBookWithPage(output.getBook(), resultPage);
context.plugins = {
resources: Plugins.listResources(plugins, prefix).toJS()
resources: Plugins.listResources(plugins, resources).toJS()
};
context.template = {
@@ -63,8 +67,8 @@ function onPage(output, page) {
return Templating.renderFile(engine, prefix + '/page.html', context)
// Write it to the disk
.then(function(html) {
return writeFile(output, filePath, html);
.then(function(tplOut) {
return writeFile(output, filePath, tplOut.getContent());
});
});
}
+54
View File
@@ -0,0 +1,54 @@
var is = require('is');
var Immutable = require('immutable');
var Promise = require('../../utils/promise');
var Api = require('../../api');
/**
Prepare plugins resources, add all output corresponding type resources
@param {Output}
@return {Promise<Output>}
*/
function prepareResources(output) {
var plugins = output.getPlugins();
var options = output.getOptions();
var type = options.get('prefix');
var state = output.getState();
var context = Api.encodeGlobal(output);
var result = Immutable.Map();
return Promise.forEach(plugins, function(plugin) {
var pluginResources = plugin.getResources(type);
return Promise()
.then(function() {
// Apply resources if is a function
if (is.fn(pluginResources)) {
return Promise()
.then(pluginResources.bind(context));
}
else {
return pluginResources;
}
})
.then(function(resources) {
result = result.set(plugin.getName(), Immutable.Map(resources));
});
})
.then(function() {
// Set output resources
state = state.merge({
resources: result
});
output = output.merge({
state: state
});
return output;
});
}
module.exports = prepareResources;
+8 -1
View File
@@ -2,11 +2,18 @@ var I18n = require('i18n-t');
var Immutable = require('immutable');
var GeneratorState = Immutable.Record({
i18n: I18n()
i18n: I18n(),
// List of plugins' resources
resources: Immutable.Map()
});
GeneratorState.prototype.getI18n = function() {
return this.get('i18n');
};
GeneratorState.prototype.getResources = function() {
return this.get('resources');
};
module.exports = GeneratorState;
+29
View File
@@ -0,0 +1,29 @@
var Immutable = require('immutable');
var Book = require('../../models/book');
var createMockFS = require('../../fs/mock');
var listAssets = require('../listAssets');
var parseGlossary = require('../parseGlossary');
describe('listAssets', function() {
pit('should not list glossary as asset', function() {
var fs = createMockFS({
'GLOSSARY.md': '# Glossary\n\n## Hello\nDescription for hello',
'assetFile.js': '',
'assets': {
'file.js': ''
}
});
var book = Book.createForFS(fs);
return parseGlossary(book)
.then(function(resultBook) {
return listAssets(resultBook, Immutable.Map());
})
.then(function(assets) {
expect(assets.size).toBe(2);
expect(assets.includes('assetFile.js'));
expect(assets.includes('assets/file.js'));
});
});
});
+6 -2
View File
@@ -17,6 +17,9 @@ function listAssets(book, pages) {
var glossary = book.getGlossary();
var glossaryFile = glossary.getFile().getPath();
var langs = book.getLanguages();
var langsFile = langs.getFile().getPath();
return timing.measure(
'parse.listAssets',
fs.listAllFiles()
@@ -25,8 +28,9 @@ function listAssets(book, pages) {
return (
book.isContentFileIgnored(file) ||
pages.has(file) ||
file !== summaryFile ||
file !== glossaryFile
file === summaryFile ||
file === glossaryFile ||
file === langsFile
);
});
})
-6
View File
@@ -1,5 +1,3 @@
var is = require('is');
var Promise = require('../utils/promise');
var Config = require('../models/config');
@@ -45,10 +43,6 @@ function parseConfig(book) {
values = validateConfig(values);
if (is.string(values.plugins)) {
values.plugins = values.plugins.split(',');
}
var config = Config.create(file, values);
return book.set('config', config);
});
-3
View File
@@ -27,9 +27,6 @@ function parseIgnore(book) {
// Skip book outputs
'_book',
'*.pdf',
'*.epub',
'*.mobi',
// Ignore files in the templates folder
'_layouts'
+1 -1
View File
@@ -43,7 +43,7 @@ function getParserByExt(ext) {
@return {Parser|undefined}
*/
function getParserForFile(filename) {
return getParser(path.extname(filename));
return getParserByExt(path.extname(filename));
}
// List all parsable extensions
+10 -1
View File
@@ -1,12 +1,21 @@
var path = require('path');
var Immutable = require('immutable');
describe('findInstalled', function() {
var findInstalled = require('../findInstalled');
pit('must list default plugins for gitbook directory', function() {
// Read gitbook-plugins from package.json
var pkg = require(path.resolve(__dirname, '../../../package.json'));
var gitbookPlugins = Immutable.Seq(pkg.dependencies)
.filter(function(v, k) {
return k.indexOf('gitbook-plugin') === 0;
})
.cacheResult();
return findInstalled(path.resolve(__dirname, '../../../'))
.then(function(plugins) {
expect(plugins.size > 7).toBeTruthy();
expect(plugins.size >= gitbookPlugins.size).toBeTruthy();
expect(plugins.has('fontsettings')).toBe(true);
expect(plugins.has('search')).toBe(true);
+9 -44
View File
@@ -1,10 +1,10 @@
jest.autoMockOff();
var PluginDependency = require('../../models/pluginDependency');
var listAll = require('../listAll');
describe('listAll', function() {
var listAll = require('../listAll');
it('must list from string', function() {
var plugins = listAll('ga,great');
it('must list default', function() {
var deps = PluginDependency.listFromString('ga,great');
var plugins = listAll(deps);
expect(plugins.size).toBe(8);
@@ -14,45 +14,9 @@ describe('listAll', function() {
expect(plugins.has('search')).toBe(true);
});
it('must list from array', function() {
var plugins = listAll(['ga', 'great']);
expect(plugins.size).toBe(8);
expect(plugins.has('ga')).toBe(true);
expect(plugins.has('great')).toBe(true);
expect(plugins.has('search')).toBe(true);
});
it('must parse version (semver)', function() {
var plugins = listAll(['ga@1.0.0', 'great@>=4.0.0']);
expect(plugins.has('ga')).toBe(true);
expect(plugins.has('great')).toBe(true);
var ga = plugins.get('ga');
expect(ga.getVersion()).toBe('1.0.0');
var great = plugins.get('great');
expect(great.getVersion()).toBe('>=4.0.0');
});
it('must parse version (git)', function() {
var plugins = listAll(['ga@git+https://github.com/GitbookIO/plugin-ga.git', 'great@git+ssh://samy@github.com/GitbookIO/plugin-ga.git']);
expect(plugins.has('ga')).toBe(true);
expect(plugins.has('great')).toBe(true);
var ga = plugins.get('ga');
expect(ga.getVersion()).toBe('git+https://github.com/GitbookIO/plugin-ga.git');
var great = plugins.get('great');
expect(great.getVersion()).toBe('git+ssh://samy@github.com/GitbookIO/plugin-ga.git');
});
it('must list from array with -', function() {
var plugins = listAll(['ga', '-great']);
var deps = PluginDependency.listFromString('ga,-great');
var plugins = listAll(deps);
expect(plugins.size).toBe(7);
@@ -61,7 +25,8 @@ describe('listAll', function() {
});
it('must remove default plugins using -', function() {
var plugins = listAll(['ga', '-search']);
var deps = PluginDependency.listFromString('ga,-search');
var plugins = listAll(deps);
expect(plugins.size).toBe(6);
+3
View File
@@ -63,6 +63,9 @@ function findInstalled(folder) {
// List all folders in node_modules
return fs.readdir(node_modules)
.fail(function() {
return Promise([]);
})
.then(function(modules) {
return Promise.serie(modules, function(module) {
// Not a gitbook-plugin
+1 -1
View File
@@ -137,7 +137,7 @@ function installPlugins(book) {
return Promise();
}
logger.info.ln('installing', plugins.size, 'plugins');
logger.info.ln('installing', plugins.size, 'plugins using npm@' + npmi.NPM_VERSION);
return Promise.forEach(plugins, function(plugin) {
return installPlugin(book, plugin);
+13 -30
View File
@@ -1,4 +1,3 @@
var is = require('is');
var Immutable = require('immutable');
var Plugin = require('../models/plugin');
@@ -8,43 +7,27 @@ var DEFAULT_PLUGINS = require('../constants/defaultPlugins');
/**
List all plugins for a book
@param {List<Plugin|String>}
@param {List<PluginDependency>} deps
@return {OrderedMap<Plugin>}
*/
function listAll(plugins) {
if (is.string(plugins)) {
plugins = Immutable.List(plugins.split(','));
}
// Convert to an ordered map
plugins = plugins.map(function(plugin) {
if (is.string(plugin)) {
plugin = Plugin.createFromString(plugin);
} else {
plugin = new Plugin(plugin);
}
return [plugin.getName(), plugin];
});
plugins = Immutable.OrderedMap(plugins);
function listAll(deps) {
// Extract list of plugins to disable (starting with -)
var toRemove = plugins.toList()
var toRemove = deps
.filter(function(plugin) {
return plugin.getName()[0] === '-';
return !plugin.isEnabled();
})
.map(function(plugin) {
return plugin.getName().slice(1);
return plugin.getName();
});
// Remove the '-'
plugins = plugins.mapKeys(function(name) {
if (name[0] === '-') {
return name.slice(1);
} else {
return name;
}
});
// Convert to an ordered map of Plugin
var plugins = deps
.map(function(dep) {
var plugin = Plugin.createFromDep(dep);
return [dep.getName(), plugin];
});
plugins = Immutable.OrderedMap(plugins);
// Append default plugins
DEFAULT_PLUGINS.forEach(function(pluginName) {
+1 -1
View File
@@ -10,7 +10,7 @@ var listAll = require('./listAll');
*/
function listForBook(book) {
var config = book.getConfig();
var plugins = config.getValue('plugins');
var plugins = config.getPluginDependencies();
return listAll(plugins);
}
+4 -4
View File
@@ -11,13 +11,13 @@ var PLUGIN_RESOURCES = require('../constants/pluginResources');
@param {String} type
@return {Map<String:List<{url, path}>}
*/
function listResources(plugins, type) {
function listResources(plugins, resources) {
return plugins.reduce(function(result, plugin) {
var npmId = plugin.getNpmID();
var resources = plugin.getResources(type);
var npmId = plugin.getNpmID();
var pluginResources = resources.get(plugin.getName());
PLUGIN_RESOURCES.forEach(function(resourceType) {
var assets = resources.get(resourceType);
var assets = pluginResources.get(resourceType);
if (!assets) return;
var list = result.get(resourceType) || Immutable.List();
+6 -6
View File
@@ -11,22 +11,22 @@ describe('ConrefsLoader', function() {
describe('Git', function() {
pit('should include content from git', function() {
return renderTemplate(engine, 'test.md', '{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md" %}')
.then(function(str) {
expect(str).toBe('Hello from git');
.then(function(out) {
expect(out.getContent()).toBe('Hello from git');
});
});
pit('should handle deep inclusion (1)', function() {
return renderTemplate(engine, 'test.md', '{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test2.md" %}')
.then(function(str) {
expect(str).toBe('First Hello. Hello from git');
.then(function(out) {
expect(out.getContent()).toBe('First Hello. Hello from git');
});
});
pit('should handle deep inclusion (2)', function() {
return renderTemplate(engine, 'test.md', '{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test3.md" %}')
.then(function(str) {
expect(str).toBe('First Hello. Hello from git');
.then(function(out) {
expect(out.getContent()).toBe('First Hello. Hello from git');
});
});
});
@@ -0,0 +1,28 @@
var TemplateEngine = require('../../models/templateEngine');
var TemplateBlock = require('../../models/templateBlock');
var replaceShortcuts = require('../replaceShortcuts');
describe('replaceShortcuts', function() {
var engine = TemplateEngine.create({
blocks:[
TemplateBlock.create('math', {
shortcuts: {
start: '$$',
end: '$$',
parsers: ['markdown']
}
})
]
});
it('should correctly replace inline matches by block', function() {
var content = replaceShortcuts(engine, 'test.md', 'Hello $$a = b$$');
expect(content).toBe('Hello {% math %}a = b{% endmath %}');
});
it('should correctly replace block matches', function() {
var content = replaceShortcuts(engine, 'test.md', 'Hello\n$$\na = b\n$$\n');
expect(content).toBe('Hello\n{% math %}\na = b\n{% endmath %}\n');
});
});
+9 -8
View File
@@ -7,25 +7,26 @@ var parsers = require('../parsers');
@param {TemplateEngine} engine
@param {String} filePath
@return {List<Shortcut>}
@return {List<TemplateShortcut>}
*/
function listShortcuts(engine, filePath) {
var blocks = engine.getBlocks();
var parser = parsers.getForFile(filePath);
if (!parser) {
return Immutable.List();
}
return blocks
.map(function(block) {
var shortcuts = block.getShortcuts();
return shortcuts.filter(function(shortcut) {
var parsers = shortcut.get('parsers');
return parsers.includes(parser.name);
});
return block.getShortcuts();
})
.flatten(1);
.filter(function(shortcuts) {
return (
shortcuts &&
shortcuts.acceptParser(parser.getName())
);
});
}
module.exports = listShortcuts;
+33 -8
View File
@@ -1,5 +1,27 @@
var Promise = require('../utils/promise');
var replaceBlocks = require('./replaceBlocks');
/**
Replace position markers of blocks by body after processing
This is done to avoid that markdown/asciidoc processer parse the block content
@param {String} content
@return {Object} {blocks: Set, content: String}
*/
function replaceBlocks(content, blocks) {
var newContent = content.replace(/\{\{\-\%([\s\S]+?)\%\-\}\}/g, function(match, key) {
var replacedWith = match;
var block = blocks.get(key);
if (block) {
replacedWith = replaceBlocks(block.get('body'), blocks);
}
return replacedWith;
});
return newContent;
}
/**
Post render a template:
@@ -7,22 +29,25 @@ var replaceBlocks = require('./replaceBlocks');
- Replace block content
@param {TemplateEngine} engine
@param {String} content
@param {TemplateOutput} content
@return {Promise<String>}
*/
function postRender(engine, content) {
var result = replaceBlocks(content);
function postRender(engine, output) {
var content = output.getContent();
var blocks = output.getBlocks();
var result = replaceBlocks(content, blocks);
return Promise.forEach(blocks, function(block) {
var post = block.get('post');
return Promise.forEach(result.blocks, function(blockType) {
var block = engine.getBlock(blockType);
var post = block.getPost();
if (!post) {
return;
}
return post();
})
.thenResolve(result.content);
.thenResolve(result);
}
module.exports = postRender;
+13 -3
View File
@@ -1,6 +1,6 @@
var Promise = require('../utils/promise');
var timing = require('../utils/timing');
var TemplateOutput = require('../models/templateOutput');
var replaceShortcuts = require('./replaceShortcuts');
/**
@@ -10,16 +10,23 @@ var replaceShortcuts = require('./replaceShortcuts');
@param {String} filePath
@param {String} content
@param {Object} context
@return {Promise<String>}
@return {Promise<TemplateOutput>}
*/
function renderTemplate(engine, filePath, content, context) {
context = context || {};
var env = engine.toNunjucks();
// Mutable objects to contains all blocks requiring post-processing
var blocks = {};
// Create nunjucks environment
var env = engine.toNunjucks(blocks);
// Replace shortcuts from plugin's blocks
content = replaceShortcuts(engine, filePath, content);
return timing.measure(
'template.render',
Promise.nfcall(
env.renderString.bind(env),
content,
@@ -28,6 +35,9 @@ function renderTemplate(engine, filePath, content, context) {
path: filePath
}
)
.then(function(content) {
return TemplateOutput.create(content, blocks);
})
);
}
+1 -1
View File
@@ -8,7 +8,7 @@ var render = require('./render');
@param {TemplateEngine} engine
@param {String} filePath
@param {Object} context
@return {Promise<String>}
@return {Promise<TemplateOutput>}
*/
function renderTemplateFile(engine, filePath, context) {
var loader = engine.getLoader();
-34
View File
@@ -1,34 +0,0 @@
var Immutable = require('immutable');
var TemplateBlock = require('../models/templateBlock');
/**
Replace position markers of blocks by body after processing
This is done to avoid that markdown/asciidoc processer parse the block content
@param {String} content
@return {Object} {blocks: Set, content: String}
*/
function replaceBlocks(content) {
var blockTypes = new Immutable.Set();
var newContent = content.replace(/\{\{\-\%([\s\S]+?)\%\-\}\}/g, function(match, key) {
var replacedWith = match;
var block = TemplateBlock.getBlockResultByKey(key);
if (block) {
var result = replaceBlocks(block.body);
blockTypes = blockTypes.add(block.name);
blockTypes = blockTypes.concat(result.blocks);
replacedWith = result.content;
}
return replacedWith;
});
return {
content: newContent,
blocks: blockTypes
};
}
module.exports = replaceBlocks;
+6 -4
View File
@@ -8,16 +8,18 @@ var listShortcuts = require('./listShortcuts');
@return {String}
*/
function applyShortcut(content, shortcut) {
var tags = shortcut.get('tag');
var start = shortcut.get('start');
var end = shortcut.get('end');
var start = shortcut.getStart();
var end = shortcut.getEnd();
var tagStart = shortcut.getStartTag();
var tagEnd = shortcut.getEndTag();
var regex = new RegExp(
escapeStringRegexp(start) + '([\\s\\S]*?[^\\$])' + escapeStringRegexp(end),
'g'
);
return content.replace(regex, function(all, match) {
return '{% ' + tags.start + ' %}' + match + '{% ' + tags.end + ' %}';
return '{% ' + tagStart + ' %}' + match + '{% ' + tagEnd + ' %}';
});
}
+9
View File
@@ -9,6 +9,15 @@ describe('LocationUtils', function() {
expect(LocationUtils.isExternal('test.md')).toBe(false);
expect(LocationUtils.isExternal('folder/test.md')).toBe(false);
expect(LocationUtils.isExternal('/folder/test.md')).toBe(false);
expect(LocationUtils.isExternal('data:image/png')).toBe(false);
});
it('should correctly test data:uri location', function() {
expect(LocationUtils.isDataURI('data:image/png')).toBe(true);
expect(LocationUtils.isDataURI('http://google.fr')).toBe(false);
expect(LocationUtils.isDataURI('https://google.fr')).toBe(false);
expect(LocationUtils.isDataURI('test.md')).toBe(false);
expect(LocationUtils.isDataURI('data.md')).toBe(false);
});
it('should correctly detect anchor location', function() {
+42 -10
View File
@@ -1,8 +1,15 @@
var is = require('is');
var childProcess = require('child_process');
var spawn = require('spawn-cmd').spawn;
var Promise = require('./promise');
// Execute a command
/**
Execute a command
@param {String} command
@param {Object} options
@return {Promise}
*/
function exec(command, options) {
var d = Promise.defer();
@@ -26,7 +33,14 @@ function exec(command, options) {
return d.promise;
}
// Spawn an executable
/**
Spawn an executable
@param {String} command
@param {Array} args
@param {Object} options
@return {Promise}
*/
function spawnCmd(command, args, options) {
var d = Promise.defer();
var child = spawn(command, args, options);
@@ -54,12 +68,29 @@ function spawnCmd(command, args, options) {
return d.promise;
}
// Transform an option object to a command line string
function escapeShellArg(s) {
s = s.replace(/"/g, '\\"');
return '"' + s + '"';
/**
Transform an option object to a command line string
@param {String|number} value
@param {String}
*/
function escapeShellArg(value) {
if (is.number(value)) {
return value;
}
value = String(value);
value = value.replace(/"/g, '\\"');
return '"' + value + '"';
}
/**
Transform a map of options into a command line arguments string
@param {Object} options
@return {String}
*/
function optionsToShellArgs(options) {
var result = [];
@@ -69,11 +100,12 @@ function optionsToShellArgs(options) {
if (value === null || value === undefined || value === false) {
continue;
}
if (value === true) {
result.push(key);
}
result.push(key + '=' + escapeShellArg(value));
if (is.bool(value)) {
result.push(key);
} else {
result.push(key + '=' + escapeShellArg(value));
}
}
return result.join(' ');
+1
View File
@@ -157,6 +157,7 @@ module.exports = {
statSync: fs.statSync,
readdir: Promise.nfbind(fs.readdir),
writeStream: writeStream,
readStream: fs.createReadStream,
copy: Promise.nfbind(cp),
copyDir: Promise.nfbind(cpr),
tmpFile: genTmpFile,
+17 -1
View File
@@ -38,7 +38,23 @@ function convertSVGBufferToPNG(buf, dest) {
});
}
// Converts a inline data: to png file
function convertInlinePNG(source, dest) {
if (!/^data\:image\/png/.test(source)) return Promise.reject(new Error('Source is not a PNG data-uri'));
var base64data = source.split('data:image/png;base64,')[1];
var buf = new Buffer(base64data, 'base64');
return fs.writeFile(dest, buf)
.then(function() {
if (fs.existsSync(dest)) return;
throw new Error('Error converting '+source+' into '+dest);
});
}
module.exports = {
convertSVGToPNG: convertSVGToPNG,
convertSVGBufferToPNG: convertSVGBufferToPNG
convertSVGBufferToPNG: convertSVGBufferToPNG,
convertInlinePNG: convertInlinePNG
};
+14 -3
View File
@@ -4,7 +4,16 @@ var path = require('path');
// Is the url an external url
function isExternal(href) {
try {
return Boolean(url.parse(href).protocol);
return Boolean(url.parse(href).protocol) && !isDataURI(href);
} catch(err) {
return false;
}
}
// Is the url an iniline data-uri
function isDataURI(href) {
try {
return Boolean(url.parse(href).protocol) && (url.parse(href).protocol === 'data:');
} catch(err) {
return false;
}
@@ -39,7 +48,7 @@ function normalize(s) {
@return {String}
*/
function toAbsolute(_href, dir, outdir) {
if (isExternal(_href)) return _href;
if (isExternal(_href) || isDataURI(_href)) return _href;
outdir = outdir == undefined? dir : outdir;
_href = normalize(_href);
@@ -68,7 +77,8 @@ function toAbsolute(_href, dir, outdir) {
@return {String}
*/
function relative(dir, file) {
return normalize(path.relative(dir, file));
var isDirectory = file.slice(-1) === '/';
return normalize(path.relative(dir, file)) + (isDirectory? '/': '');
}
/**
@@ -97,6 +107,7 @@ function areIdenticalPaths(p1, p2) {
module.exports = {
areIdenticalPaths: areIdenticalPaths,
isDataURI: isDataURI,
isExternal: isExternal,
isRelative: isRelative,
isAnchor: isAnchor,
+5
View File
@@ -1,6 +1,11 @@
var Q = require('q');
var Immutable = require('immutable');
// Debugging for long stack traces
if (global.__DEV__ || process.env.DEBUG) {
Q.longStackSupport = true;
}
/**
Reduce an array to a promise
+18 -16
View File
@@ -1,12 +1,12 @@
{
"name": "gitbook",
"version": "3.0.0-pre.10",
"version": "3.0.0-pre.14",
"homepage": "https://www.gitbook.com",
"description": "Library and cmd utility to generate GitBooks",
"main": "lib/index.js",
"browser": "./lib/browser.js",
"dependencies": {
"bash-color": "0.0.3",
"bash-color": "0.0.4",
"cheerio": "0.20.0",
"chokidar": "1.4.3",
"cp": "0.2.0",
@@ -18,23 +18,22 @@
"error": "7.0.2",
"escape-html": "^1.0.3",
"escape-string-regexp": "1.0.5",
"eslint": "^2.2.0",
"extend": "^3.0.0",
"fresh-require": "1.0.3",
"front-matter": "2.0.7",
"gitbook-asciidoc": "1.1.0",
"gitbook-markdown": "1.2.0",
"gitbook-asciidoc": "1.1.4",
"gitbook-markdown": "1.2.3",
"gitbook-plugin-fontsettings": "1.0.3",
"gitbook-plugin-highlight": "2.0.2",
"gitbook-plugin-livereload": "0.0.1",
"gitbook-plugin-lunr": "1.0.0",
"gitbook-plugin-lunr": "1.1.0",
"gitbook-plugin-search": "2.2.1",
"gitbook-plugin-sharing": "1.0.2",
"gitbook-plugin-theme-default": "1.0.0-pre.9",
"gitbook-plugin-theme-default": "1.0.0-pre.10",
"github-slugid": "1.0.1",
"graceful-fs": "4.1.3",
"i18n-t": "1.0.0",
"ignore": "3.1.1",
"ignore": "3.1.2",
"immutable": "^3.8.1",
"is": "^3.1.0",
"json-schema-defaults": "0.1.1",
@@ -42,16 +41,16 @@
"juice": "1.10.0",
"merge-defaults": "0.2.1",
"mkdirp": "0.5.1",
"moment": "2.12.0",
"npm": "3.8.6",
"npmi": "1.0.1",
"moment": "2.13.0",
"npm": "3.8.8",
"npmi": "2.0.1",
"nunjucks": "2.4.2",
"nunjucks-do": "1.0.0",
"object-path": "^0.9.2",
"omit-keys": "^0.1.0",
"q": "1.4.1",
"read-installed": "^4.0.3",
"request": "2.70.0",
"request": "2.72.0",
"resolve": "1.1.7",
"rmdir": "1.2.0",
"semver": "5.1.0",
@@ -59,11 +58,11 @@
"spawn-cmd": "0.0.2",
"tiny-lr": "0.2.1",
"tmp": "0.0.28",
"urijs": "1.17.1"
"urijs": "1.18.0"
},
"devDependencies": {
"eslint": "2.7.0",
"jest-cli": "^11.0.2"
"eslint": "2.9.0",
"jest-cli": "12.0.2"
},
"scripts": {
"test": "node_modules/.bin/jest --bail",
@@ -98,6 +97,9 @@
],
"jest": {
"automock": false,
"setupTestFrameworkScriptFile": "<rootDir>/jest/customMatchers.js"
"setupTestFrameworkScriptFile": "<rootDir>/jest/customMatchers.js",
"globals": {
"__DEV__": true
}
}
}