diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/blocks.js | 11 | ||||
-rw-r--r-- | lib/book.js | 29 | ||||
-rw-r--r-- | lib/configuration.js | 6 | ||||
-rw-r--r-- | lib/conrefs_loader.js | 64 | ||||
-rw-r--r-- | lib/generator.js | 6 | ||||
-rw-r--r-- | lib/generators/website.js | 2 | ||||
-rw-r--r-- | lib/pluginslist.js | 8 | ||||
-rw-r--r-- | lib/template.js | 150 | ||||
-rw-r--r-- | lib/utils/code.js | 36 | ||||
-rw-r--r-- | lib/utils/git.js | 21 | ||||
-rw-r--r-- | lib/utils/navigation.js | 9 | ||||
-rw-r--r-- | lib/utils/page.js | 18 | ||||
-rw-r--r-- | lib/utils/path.js | 39 |
13 files changed, 271 insertions, 128 deletions
diff --git a/lib/blocks.js b/lib/blocks.js new file mode 100644 index 0000000..92097a7 --- /dev/null +++ b/lib/blocks.js @@ -0,0 +1,11 @@ +var _ = require('lodash'); + +module.exports = { + // Return non-parsed html + // since blocks are by default non-parsable, a simple identity method works fine + html: _.identity, + + // Highlight a code block + // This block can be extent by plugins + code: _.identity +}; diff --git a/lib/book.js b/lib/book.js index b306c51..08dd6dc 100644 --- a/lib/book.js +++ b/lib/book.js @@ -10,6 +10,7 @@ var fs = require("./utils/fs"); var parseNavigation = require("./utils/navigation"); var parseProgress = require("./utils/progress"); var pageUtil = require("./utils/page"); +var pathUtil = require("./utils/path"); var batch = require("./utils/batch"); var links = require("./utils/links"); var i18n = require("./utils/i18n"); @@ -630,21 +631,26 @@ Book.prototype.findFile = function(filename) { // Check if a file exists in the book Book.prototype.fileExists = function(filename) { return fs.exists( - path.join(this.root, filename) + this.resolve(filename) ); }; +// Check if a file path is inside the book +Book.prototype.fileIsInBook = function(filename) { + return pathUtil.isInRoot(this.root, filename); +}; + // Read a file Book.prototype.readFile = function(filename) { return fs.readFile( - path.join(this.root, filename), + this.resolve(filename), { encoding: "utf8" } ); }; // Return stat for a file Book.prototype.statFile = function(filename) { - return fs.stat(path.join(this.root, filename)); + return fs.stat(this.resolve(filename)); }; // List all files in the book @@ -702,9 +708,20 @@ Book.prototype.isEntryPoint = function(fp) { return fp == this.readmeFile; }; -// Resolve a path in book -Book.prototype.resolve = function(p) { - return path.resolve(this.root, p); +// Alias to book.config.get +Book.prototype.getConfig = function(key, def) { + return this.config.get(key, def); +}; + +// Resolve a path in the book source +// Enforce that the output path in the root folder +Book.prototype.resolve = function() { + return pathUtil.resolveInRoot.apply(null, [this.root].concat(_.toArray(arguments))); +}; + +// Convert an abslute path into a relative path to this +Book.prototype.relative = function(p) { + return path.relative(this.root, p); }; // Normalize a path to .html and convert README -> index diff --git a/lib/configuration.js b/lib/configuration.js index 3dee9f5..acff1c1 100644 --- a/lib/configuration.js +++ b/lib/configuration.js @@ -8,7 +8,7 @@ var fs = require("./utils/fs"); var i18n = require("./utils/i18n"); // Default plugins added to each books -var defaultsPlugins = []; +var defaultsPlugins = ['highlight']; // Normalize a list of plugins to use function normalizePluginsList(plugins) { @@ -100,7 +100,7 @@ Configuration.prototype.load = function() { try { configPath = require.resolve( - path.resolve(that.book.root, that.options.configFile) + that.book.resolve(that.options.configFile) ); // Invalidate node.js cache for livreloading @@ -125,7 +125,7 @@ Configuration.prototype.load = function() { that.book.log.warn.ln("gitbook version specified in your book.json might be too strict for future patches, \""+(_.first(pkg.version.split("."))+".x.x")+"\" is more adequate"); } - that.options.output = path.resolve(that.options.output || path.join(that.book.root, "_book")); + that.options.output = path.resolve(that.options.output || that.book.resolve("_book")); that.options.plugins = normalizePluginsList(that.options.plugins); that.options.defaultsPlugins = normalizePluginsList(that.options.defaultsPlugins || ""); that.options.plugins = _.union(that.options.plugins, that.options.defaultsPlugins); diff --git a/lib/conrefs_loader.js b/lib/conrefs_loader.js new file mode 100644 index 0000000..72dce8a --- /dev/null +++ b/lib/conrefs_loader.js @@ -0,0 +1,64 @@ +var Q = require("q"); +var path = require("path"); +var nunjucks = require("nunjucks"); + +var git = require("./utils/git"); +var fs = require("./utils/fs"); +var pathUtil = require("./utils/path"); + +// The loader should handle relative and git url +var BookLoader = nunjucks.Loader.extend({ + async: true, + + init: function(book) { + this.book = book; + }, + + getSource: function(fileurl, callback) { + var that = this; + + git.resolveFile(fileurl) + .then(function(filepath) { + // Is local file + if (!filepath) filepath = path.resolve(fileurl); + else that.book.log.debug.ln("resolve from git", fileurl, "to", filepath); + + // Read file from absolute path + return fs.readFile(filepath) + .then(function(source) { + return { + src: source.toString(), + path: filepath + } + }); + }) + .nodeify(callback); + }, + + resolve: function(from, to) { + // If origin is in the book, we enforce result file to be in the book + if (this.book.fileIsInBook(from)) { + return this.book.resolve( + this.book.relative(path.dirname(from)), + to + ); + } + + // If origin is in a git repository, we resolve file in the git repository + var gitRoot = git.resolveRoot(from); + if (gitRoot) { + return pathUtil.resolveInRoot(gitRoot, to); + } + + // If origin is not in the book (include from a git content ref) + return path.resolve(path.dirname(from), to); + }, + + // Handle all files as relative, so that nunjucks pass responsability to "resolve" + // Only git urls are considered as absolute + isRelative: function(filename) { + return !git.checkUrl(filename); + } +}); + +module.exports = BookLoader; diff --git a/lib/generator.js b/lib/generator.js index c809de2..407afa5 100644 --- a/lib/generator.js +++ b/lib/generator.js @@ -36,7 +36,7 @@ BaseGenerator.prototype.convertFile = function(input) { // Copy file to the output (non parsable) BaseGenerator.prototype.transferFile = function(input) { return fs.copy( - path.join(this.book.root, input), + this.book.resolve(input), path.join(this.options.output, input) ); }; @@ -53,8 +53,8 @@ BaseGenerator.prototype.copyCover = function() { var that = this; return Q.all([ - fs.copy(path.join(that.book.root, "cover.jpg"), path.join(that.options.output, "cover.jpg")), - fs.copy(path.join(that.book.root, "cover_small.jpg"), path.join(that.options.output, "cover_small.jpg")) + fs.copy(that.book.resolve("cover.jpg"), path.join(that.options.output, "cover.jpg")), + fs.copy(that.book.resolve("cover_small.jpg"), path.join(that.options.output, "cover_small.jpg")) ]) .fail(function() { // If orignaly from multi-lang, try copy from parent diff --git a/lib/generators/website.js b/lib/generators/website.js index de833d3..675092f 100644 --- a/lib/generators/website.js +++ b/lib/generators/website.js @@ -50,7 +50,7 @@ Generator.prototype.prepareStyles = function() { this.styles = _.chain(this.styles) .map(function(style) { var stylePath = that.options.styles[style]; - if (stylePath && fs.existsSync(path.resolve(that.book.root, stylePath))) { + if (stylePath && fs.existsSync(that.book.resolve(stylePath))) { return [style, stylePath]; } return null; diff --git a/lib/pluginslist.js b/lib/pluginslist.js index 227a013..37dbd41 100644 --- a/lib/pluginslist.js +++ b/lib/pluginslist.js @@ -79,14 +79,10 @@ PluginsList.prototype.load = function(plugin, options) { } // Extract filters - _.each(plugin.getFilters(), function(filterFunc, filterName) { - that.book.template.addFilter(filterName, filterFunc); - }); + that.book.template.addFilters(plugin.getFilters()); // Extract blocks - _.each(plugin.getBlocks(), function(block, blockName) { - that.book.template.addBlock(blockName, block); - }); + that.book.template.addBlocks(plugin.getBlocks()); return _.reduce(_.keys(that.namespaces), function(prev, namespaceName) { return prev.then(function() { diff --git a/lib/template.js b/lib/template.js index 8014405..9f01d3c 100644 --- a/lib/template.js +++ b/lib/template.js @@ -4,45 +4,16 @@ var path = require("path"); var nunjucks = require("nunjucks"); var escapeStringRegexp = require("escape-string-regexp"); -var git = require("./utils/git"); -var fs = require("./utils/fs"); var batch = require("./utils/batch"); var pkg = require("../package.json"); +var defaultBlocks = require("./blocks"); +var BookLoader = require("./conrefs_loader") - -// The loader should handle relative and git url -var BookLoader = nunjucks.Loader.extend({ - async: true, - - init: function(book) { - this.book = book; - }, - - getSource: function(fileurl, callback) { - var that = this; - - git.resolveFile(fileurl) - .then(function(filepath) { - // Is local file - if (!filepath) filepath = path.resolve(that.book.root, fileurl); - else that.book.log.debug.ln("resolve from git", fileurl, "to", filepath) - - // Read file from absolute path - return fs.readFile(filepath) - .then(function(source) { - return { - src: source.toString(), - path: filepath - } - }); - }) - .nodeify(callback); - }, - - resolve: function(from, to) { - return path.resolve(path.dirname(from), to); - } -}); +// Normalize result from a block +function normBlockResult(blk) { + if (_.isString(blk)) blk = { body: blk }; + return blk; +} var TemplateEngine = function(book) { @@ -71,22 +42,21 @@ var TemplateEngine = function(book) { // List of tags shortcuts this.shortcuts = []; - // Map of blocks + // Map of blocks bodies (that requires post-processing) + this.blockBodies = {}; + + // Map of added blocks this.blocks = {}; // Bind methods _.bindAll(this); - // Default block "html" that return html not parsed - this.addBlock("html", { - process: _.identity - }); + // Add default blocks + this.addBlocks(defaultBlocks); }; -// Process a block in a context +// Process the result of block in a context TemplateEngine.prototype.processBlock = function(blk) { - if (_.isString(blk)) blk = { body: blk }; - blk = _.defaults(blk, { parse: false, post: undefined @@ -96,7 +66,7 @@ TemplateEngine.prototype.processBlock = function(blk) { var toAdd = (!blk.parse) || (blk.post != undefined); // Add to global map - if (toAdd) this.blocks[blk.id] = blk; + if (toAdd) this.blockBodies[blk.id] = blk; //Parsable block, just return it if (blk.parse) { @@ -113,7 +83,7 @@ TemplateEngine.prototype.replaceBlocks = function(content) { var that = this; return content.replace(/\@\%\@([\s\S]+?)\@\%\@/g, function(match, key) { - var blk = that.blocks[key]; + var blk = that.blockBodies[key]; if (!blk) return match; var body = blk.body; @@ -160,9 +130,41 @@ TemplateEngine.prototype.addFilter = function(filterName, func) { return true; }; +// Add multiple filters +TemplateEngine.prototype.addFilters = function(filters) { + _.each(filters, function(filter, name) { + this.addFilter(name, filter); + }, this); +}; + +// Return nunjucks extension name of a block +TemplateEngine.prototype.blockExtName = function(name) { + return 'Block'+name+'Extension'; +}; + +// Test if a block is defined +TemplateEngine.prototype.hasBlock = function(name) { + return this.env.hasExtension(this.blockExtName(name)); +}; + +// Remove a block +TemplateEngine.prototype.removeBlock = function(name) { + if (!this.hasBlock(name)) return; + + // Remove nunjucks extension + this.env.removeExtension(this.blockExtName(name)); + + // Cleanup shortcuts + this.shortcuts = _.reject(this.shortcuts, { + block: name + }); +}; + // Add a block TemplateEngine.prototype.addBlock = function(name, block) { - var that = this; + var that = this, Ext, extName; + + if (_.isFunction(block)) block = { process: block }; block = _.defaults(block || {}, { shortcuts: [], @@ -171,13 +173,17 @@ TemplateEngine.prototype.addBlock = function(name, block) { blocks: [] }); - var extName = 'Block'+name+'Extension'; - if (this.env.getExtension(extName)) { + var extName = this.blockExtName(name); + + if (this.hasBlock(name) && !defaultBlocks[name]) { this.log.warn.ln("conflict in blocks, '"+name+"' is already defined"); - return false; } + // Cleanup previous block + this.removeBlock(name); + this.log.debug.ln("add block '"+name+"'"); + this.blocks[name] = block; var Ext = function () { this.tags = [name]; @@ -260,11 +266,9 @@ TemplateEngine.prototype.addBlock = function(name, block) { }; }); - var func = that.bindContext(block.process); - Q() .then(function() { - return func.call(context, { + return that.applyBlock(name, { body: body(), args: args, kwargs: kwargs, @@ -278,7 +282,6 @@ TemplateEngine.prototype.addBlock = function(name, block) { }; }; - // Add the Extension this.env.addExtension(extName, new Ext()); @@ -287,6 +290,7 @@ TemplateEngine.prototype.addBlock = function(name, block) { _.each(block.shortcuts, function(shortcut) { this.log.debug.ln("add template shortcut from '"+shortcut.start+"' to block '"+name+"' for parsers ", shortcut.parsers); this.shortcuts.push({ + block: name, parsers: shortcut.parsers, start: shortcut.start, end: shortcut.end, @@ -298,6 +302,40 @@ TemplateEngine.prototype.addBlock = function(name, block) { }, this); }; +// Add multiple blocks +TemplateEngine.prototype.addBlocks = function(blocks) { + _.each(blocks, function(block, name) { + this.addBlock(name, block); + }, this); +}; + +// Apply a block to some content +// This method result depends on the type of block (async or sync) +TemplateEngine.prototype.applyBlock = function(name, blk) { + var func, block, func, r; + + block = this.blocks[name]; + if (!block) throw new Error('Block not found "'+name+'"'); + if (_.isString(blk)) { + blk = { + body: blk + }; + } + + blk = _.defaults(blk, { + args: [], + kwargs: {}, + blocks: [] + }); + + // Bind and call block processor + func = this.bindContext(block.process); + r = func.call(context, blk); + + if (Q.isPromise(r)) return r.then(normBlockResult); + else return normBlockResult(r); +}; + // Apply a shortcut to a string TemplateEngine.prototype._applyShortcut = function(parser, content, shortcut) { if (!_.contains(shortcut.parsers, parser)) return content; @@ -383,7 +421,7 @@ TemplateEngine.prototype.postProcess = function(content) { return Q(content) .then(that.replaceBlocks) .then(function(_content) { - return batch.execEach(that.blocks, { + return batch.execEach(that.blockBodies, { max: 20, fn: function(blk, blkId) { return Q() @@ -392,7 +430,7 @@ TemplateEngine.prototype.postProcess = function(content) { return blk.post(); }) .then(function() { - delete that.blocks[blkId]; + delete that.blockBodies[blkId]; }); } }) diff --git a/lib/utils/code.js b/lib/utils/code.js deleted file mode 100644 index 0d98869..0000000 --- a/lib/utils/code.js +++ /dev/null @@ -1,36 +0,0 @@ -var hljs = require('highlight.js'); - -var MAP = { - 'py': 'python', - 'js': 'javascript', - 'json': 'javascript', - 'rb': 'ruby', - 'csharp': 'cs', -}; - -function normalize(lang) { - if(!lang) { return null; } - - var lower = lang.toLowerCase(); - return MAP[lower] || lower; -} - -function highlight(lang, code) { - if(!lang) return code; - - // Normalize lang - lang = normalize(lang); - - try { - return hljs.highlight(lang, code).value; - } catch(e) { } - - return code; -} - -// Exports -module.exports = { - highlight: highlight, - normalize: normalize, - MAP: MAP -}; diff --git a/lib/utils/git.js b/lib/utils/git.js index 5f17395..6eb9681 100644 --- a/lib/utils/git.js +++ b/lib/utils/git.js @@ -6,6 +6,7 @@ var path = require("path"); var crc = require("crc"); var exec = Q.denodeify(require("child_process").exec); var URI = require("URIjs"); +var pathUtil = require("./path"); var fs = require("./fs"); @@ -89,7 +90,6 @@ function cloneGitRepo(host, ref) { }); } - // Get file from a git repo function resolveFileFromGit(giturl) { if (_.isString(giturl)) giturl = parseGitUrl(giturl); @@ -104,9 +104,26 @@ function resolveFileFromGit(giturl) { }); }; +// Return root of git repo from a filepath +function resolveGitRoot(filepath) { + var relativeToGit, repoId + + // No git repo cloned, or file is not in a git repository + if (!GIT_TMP || !pathUtil.isInRoot(GIT_TMP, filepath)) return null; + + // Extract first directory (is the repo id) + relativeToGit = path.relative(GIT_TMP, filepath); + repoId = _.first(relativeToGit.split(path.sep)); + if (!repoId) return; + + // Return an absolute file + return path.resolve(GIT_TMP, repoId); +}; + module.exports = { checkUrl: checkGitUrl, parseUrl: parseGitUrl, - resolveFile: resolveFileFromGit + resolveFile: resolveFileFromGit, + resolveRoot: resolveGitRoot }; diff --git a/lib/utils/navigation.js b/lib/utils/navigation.js index af9330d..d825c2c 100644 --- a/lib/utils/navigation.js +++ b/lib/utils/navigation.js @@ -27,7 +27,7 @@ function navigation(summary, files) { files = _.isArray(files) ? files : (_.isString(files) ? [files] : null); // List of all navNodes - // Flatten chapters, then add in default README node if needed etc ... + // Flatten chapters var navNodes = flattenChapters(summary.chapters); // Mapping of prev/next for a give path @@ -39,8 +39,7 @@ function navigation(summary, files) { if(!current.exists) return null; // Find prev - prev = _.chain(navNodes) - .slice(0, i) + prev = _.chain(navNodes.slice(0, i)) .reverse() .find(function(node) { return node.exists && !node.external; @@ -48,14 +47,12 @@ function navigation(summary, files) { .value(); // Find next - next = _.chain(navNodes) - .slice(i+1) + next = _.chain(navNodes.slice(i+1)) .find(function(node) { return node.exists && !node.external; }) .value(); - return [current.path, { index: i, title: current.title, diff --git a/lib/utils/page.js b/lib/utils/page.js index e2a7cd5..5b4eca8 100644 --- a/lib/utils/page.js +++ b/lib/utils/page.js @@ -11,7 +11,6 @@ var links = require('./links'); var imgUtils = require('./images'); var fs = require('./fs'); var batch = require('./batch'); -var code = require('./code'); var parsableExtensions = require('gitbook-parsers').extensions; @@ -224,7 +223,7 @@ function normalizeHtml(src, options) { // Highlight code blocks $("code").each(function() { - // Extract language + // Normalize language var lang = _.chain( ($(this).attr("class") || "").split(" ") ) @@ -241,14 +240,15 @@ function normalizeHtml(src, options) { .first() .value(); - if (lang) { - var html = code.highlight( - lang, - $(this).text() - ); + var source = $(this).text(); + var html = options.book.template.applyBlock('code', { + body: source, + kwargs: { + language: lang + } + }).body; - $(this).html(html); - } + $(this).html(html); }); // Replace glossary terms diff --git a/lib/utils/path.js b/lib/utils/path.js new file mode 100644 index 0000000..d5b98f7 --- /dev/null +++ b/lib/utils/path.js @@ -0,0 +1,39 @@ +var _ = require("lodash"); +var path = require('path'); + +// Return true if file path is inside a folder +function isInRoot(root, filename) { + filename = path.normalize(filename); + return (filename.substr(0, root.length) === root); +} + +// Resolve paths in a specific folder +// Throw error if file is outside this folder +function resolveInRoot(root) { + var input = _.chain(arguments) + .toArray() + .slice(1) + .reduce(function(current, p, i) { + // Handle path relative to book root ('/README.md') + if (p[0] == '/' || p[0] == '\\') return p.slice(1); + + return current? path.join(current, p) : path.normalize(p); + }, '') + .value(); + + var result = path.resolve(root, input); + + if (!isInRoot(root, result)) { + err = new Error("EACCESS: '" + result + "' not in '" + root + "'"); + err.code = "EACCESS"; + throw err; + } + + return result +}; + + +module.exports = { + isInRoot: isInRoot, + resolveInRoot: resolveInRoot +}; |