Compare commits

...

24 Commits

Author SHA1 Message Date
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
49 changed files with 820 additions and 211 deletions
+21
View File
@@ -1,3 +1,4 @@
var path = require('path');
var Promise = require('../utils/promise');
var PathUtils = require('../utils/path');
var fs = require('../utils/fs');
@@ -67,6 +68,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 +107,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}
+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
});
};
+31 -2
View File
@@ -13,11 +13,9 @@ describe('TemplateBlock', function() {
// 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,6 +30,37 @@ describe('TemplateBlock', function() {
});
});
describe('getShortcuts', function() {
it('must return undefined if no shortcuts', function() {
var templateBlock = TemplateBlock.create('sayhello', function(block) {
return '<p>Hello, World!</p>';
});
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('must create a valid nunjucks extension', function() {
var templateBlock = TemplateBlock.create('sayhello', function(block) {
+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
+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));
};
+33 -1
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) {
@@ -129,6 +129,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 +209,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;
+28 -45
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,10 +22,7 @@ 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,
shortcuts: Immutable.Map(),
parse: true
}, 'TemplateBlock');
@@ -35,10 +31,6 @@ TemplateBlock.prototype.getName = function() {
return this.get('name');
};
TemplateBlock.prototype.getPost = function() {
return this.get('post');
};
TemplateBlock.prototype.getParse = function() {
return this.get('parse');
};
@@ -55,8 +47,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 +76,7 @@ TemplateBlock.prototype.getExtensionName = function() {
@return {Nunjucks.Extension}
*/
TemplateBlock.prototype.toNunjucksExt = function(mainContext) {
TemplateBlock.prototype.toNunjucksExt = function(mainContext, blocksOutput) {
var that = this;
var name = this.getName();
var endTag = this.getEndTag();
@@ -183,7 +186,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 +212,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,15 +237,17 @@ 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) {
TemplateBlock.prototype.blockResultToHtml = function(result, blocksOutput) {
var parse = this.getParse();
var indexedKey;
var toIndex = (!parse) || (this.getPost() !== undefined);
var toIndex = (!parse) || (result.post !== undefined);
if (toIndex) {
indexedKey = TemplateBlock.indexBlockResult(result);
indexedKey = genKey();
blocksOutput[indexedKey] = result;
}
// Parsable block, just return it
@@ -255,29 +260,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 +274,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;
@@ -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',
+2 -2
View File
@@ -30,8 +30,8 @@ function getPDFTemplate(output, type) {
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
}
+2 -2
View File
@@ -29,8 +29,8 @@ function writeSummary(output) {
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());
});
}
+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
+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
);
});
})
-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
+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
+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;
+32 -7
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) {
function postRender(engine, output) {
var content = output.getContent();
var blocks = output.getBlocks();
var result = replaceBlocks(content);
return Promise.forEach(result.blocks, function(blockType) {
var block = engine.getBlock(blockType);
var post = block.getPost();
return Promise.forEach(blocks, function(block) {
var post = block.get('post');
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 + ' %}';
});
}
+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,
+2 -1
View File
@@ -77,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? '/': '');
}
/**
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gitbook",
"version": "3.0.0-pre.11",
"version": "3.0.0-pre.13",
"homepage": "https://www.gitbook.com",
"description": "Library and cmd utility to generate GitBooks",
"main": "lib/index.js",