diff options
Diffstat (limited to 'lib/output2/website')
-rw-r--r-- | lib/output2/website/index.js | 225 | ||||
-rw-r--r-- | lib/output2/website/templateEnv.js | 95 | ||||
-rw-r--r-- | lib/output2/website/themeLoader.js | 127 |
3 files changed, 447 insertions, 0 deletions
diff --git a/lib/output2/website/index.js b/lib/output2/website/index.js new file mode 100644 index 0000000..0a8618c --- /dev/null +++ b/lib/output2/website/index.js @@ -0,0 +1,225 @@ +var _ = require('lodash'); +var path = require('path'); +var util = require('util'); +var I18n = require('i18n-t'); + +var Promise = require('../../utils/promise'); +var location = require('../../utils/location'); +var fs = require('../../utils/fs'); +var conrefsLoader = require('../conrefs'); +var Output = require('../base'); +var setupTemplateEnv = require('./templateEnv'); + +function _WebsiteOutput() { + Output.apply(this, arguments); + + // Nunjucks environment + this.env; + + // Plugin instance for the main theme + this.theme; + + // Plugin instance for the default theme + this.defaultTheme; + + // Resources loaded from plugins + this.resources; + + // i18n for themes + this.i18n = new I18n(); +} +util.inherits(_WebsiteOutput, Output); + +var WebsiteOutput = conrefsLoader(_WebsiteOutput); + +// Name of the generator +// It's being used as a prefix for templates +WebsiteOutput.prototype.name = 'website'; + +// Load and setup the theme +WebsiteOutput.prototype.prepare = function() { + var that = this; + + return Promise() + .then(function() { + return WebsiteOutput.super_.prototype.prepare.apply(that); + }) + + .then(function() { + // This list is ordered to give priority to templates in the book + var searchPaths = _.pluck(that.plugins.list(), 'root'); + + // The book itself can contains a "_layouts" folder + searchPaths.unshift(that.book.root); + + // Load i18n + _.each(searchPaths.concat().reverse(), function(searchPath) { + var i18nRoot = path.resolve(searchPath, '_i18n'); + + if (!fs.existsSync(i18nRoot)) return; + that.i18n.load(i18nRoot); + }); + + that.searchPaths = searchPaths; + }) + + // Copy assets from themes before copying files from book + .then(function() { + if (that.book.isLanguageBook()) return; + + // Assets from the book are already copied + // Copy assets from plugins (start with default plugins) + return Promise.serie(that.plugins.list().reverse(), function(plugin) { + // Copy assets only if exists (don't fail otherwise) + var assetFolder = path.join(plugin.root, '_assets', that.name); + if (!fs.existsSync(assetFolder)) return; + + that.log.debug.ln('copy assets from theme', assetFolder); + return fs.copyDir( + assetFolder, + that.resolve('gitbook'), + { + deleteFirst: false, + overwrite: true, + confirm: true + } + ); + }); + }) + + // Load resources for plugins + .then(function() { + return that.plugins.getResources(that.name) + .then(function(resources) { + that.resources = resources; + }); + }); +}; + +// Write a page (parsable file) +WebsiteOutput.prototype.onPage = function(page) { + var that = this; + + // Parse the page + return page.toHTML(this) + + // Render the page template with the same context as the json output + .then(function() { + return that.render('page', that.outputPath(page.path), page.getOutputContext(that)); + }); +}; + +// Finish generation, create ebook using ebook-convert +WebsiteOutput.prototype.finish = function() { + var that = this; + + return Promise() + .then(function() { + return WebsiteOutput.super_.prototype.finish.apply(that); + }) + + // Copy assets from plugins + .then(function() { + if (that.book.isLanguageBook()) return; + return that.plugins.copyResources(that.name, that.resolve('gitbook')); + }) + + // Generate homepage to select languages + .then(function() { + if (!that.book.isMultilingual()) return; + return that.outputMultilingualIndex(); + }); +}; + +// ----- Utilities ---- + +// Write multi-languages index +WebsiteOutput.prototype.outputMultilingualIndex = function() { + var that = this; + + return that.render('languages', 'index.html', that.getContext()); +}; + +/* + Render a template as an HTML string + Templates are stored in `_layouts` folders + + + @param {String} tpl: template name (ex: "page") + @param {String} outputFile: filename to write, relative to the output + @param {Object} context: context for the page + @return {Promise} +*/ +WebsiteOutput.prototype.renderAsString = function(tpl, context) { + // Calcul template name + var filename = this.templateName(tpl); + + context = _.extend(context, { + plugins: { + resources: this.resources + }, + + options: this.opts + }); + + // Create environment + var env = setupTemplateEnv(this, context); + + return Promise.nfcall(env.render.bind(env), filename, context); +}; + +/* + Render a template using nunjucks + Templates are stored in `_layouts` folders + + + @param {String} tpl: template name (ex: "page") + @param {String} outputFile: filename to write, relative to the output + @param {Object} context: context for the page + @return {Promise} +*/ +WebsiteOutput.prototype.render = function(tpl, outputFile, context) { + var that = this; + + // Calcul relative path to the root + var outputDirName = path.dirname(outputFile); + var basePath = location.normalize(path.relative(outputDirName, './')); + + // Setup complete context + context = _.extend(context, { + basePath: basePath, + + template: { + getJSContext: function() { + return { + page: _.omit(context.page, 'content'), + config: context.config, + file: context.file, + gitbook: context.gitbook, + basePath: basePath, + book: { + language: context.book.language + } + }; + } + } + }); + + return this.renderAsString(tpl, context) + .then(function(html) { + return that.writeFile( + outputFile, + html + ); + }); +}; + +// Return a complete name for a template +WebsiteOutput.prototype.templateName = function(name) { + return path.join(this.name, name+'.html'); +}; + +module.exports = WebsiteOutput; + + + diff --git a/lib/output2/website/templateEnv.js b/lib/output2/website/templateEnv.js new file mode 100644 index 0000000..d385108 --- /dev/null +++ b/lib/output2/website/templateEnv.js @@ -0,0 +1,95 @@ +var _ = require('lodash'); +var nunjucks = require('nunjucks'); +var path = require('path'); +var fs = require('fs'); +var DoExtension = require('nunjucks-do')(nunjucks); + + +var location = require('../../utils/location'); +var defaultFilters = require('../../template/filters'); + +var ThemeLoader = require('./themeLoader'); + +// Directory for a theme with the templates +function templatesPath(dir) { + return path.join(dir, '_layouts'); +} + +/* + Create and setup at Nunjucks template environment + + @return {Nunjucks.Environment} +*/ +function setupTemplateEnv(output, context) { + context = _.defaults(context || {}, { + // Required by ThemeLoader + template: {} + }); + + var loader = new ThemeLoader( + _.map(output.searchPaths, templatesPath) + ); + var env = new nunjucks.Environment(loader); + + env.addExtension('DoExtension', new DoExtension()); + + // Add context as global + _.each(context, function(value, key) { + env.addGlobal(key, value); + }); + + // Add GitBook default filters + _.each(defaultFilters, function(fn, filter) { + env.addFilter(filter, fn); + }); + + // Translate using _i18n locales + env.addFilter('t', function t(s) { + return output.i18n.t(output.book.config.get('language'), s); + }); + + // Transform an absolute path into a relative path + // using this.ctx.page.path + env.addFilter('resolveFile', function resolveFile(href) { + return location.normalize(output.resolveForPage(context.file.path, href)); + }); + + // Test if a file exists + env.addFilter('fileExists', function fileExists(href) { + return fs.existsSync(output.resolve(href)); + }); + + // Transform a '.md' into a '.html' (README -> index) + env.addFilter('contentURL', function contentURL(s) { + return output.toURL(s); + }); + + // Get an article using its path + env.addFilter('getArticleByPath', function getArticleByPath(s) { + var article = output.book.summary.getArticle(s); + if (!article) return undefined; + + return article.getContext(); + }); + + // Relase path to an asset + env.addFilter('resolveAsset', function resolveAsset(href) { + href = path.join('gitbook', href); + + // Resolve for current file + if (context.file) { + href = output.resolveForPage(context.file.path, '/' + href); + } + + // Use assets from parent + if (output.book.isLanguageBook()) { + href = path.join('../', href); + } + + return location.normalize(href); + }); + + return env; +} + +module.exports = setupTemplateEnv; diff --git a/lib/output2/website/themeLoader.js b/lib/output2/website/themeLoader.js new file mode 100644 index 0000000..774a39e --- /dev/null +++ b/lib/output2/website/themeLoader.js @@ -0,0 +1,127 @@ +var _ = require('lodash'); +var fs = require('fs'); +var path = require('path'); +var nunjucks = require('nunjucks'); + +/* + Nunjucks loader similar to FileSystemLoader, but avoid infinite looping +*/ + +/* + Return true if a filename is relative. +*/ +function isRelative(filename) { + return (filename.indexOf('./') === 0 || filename.indexOf('../') === 0); +} + +var ThemeLoader = nunjucks.Loader.extend({ + init: function(searchPaths) { + this.searchPaths = _.map(searchPaths, path.normalize); + }, + + /* + Read source of a resolved filepath + + @param {String} + @return {Object} + */ + getSource: function(fullpath) { + if (!fullpath) return null; + + fullpath = this.resolve(null, fullpath); + var templateName = this.getTemplateName(fullpath); + + if(!fullpath) { + return null; + } + + var src = fs.readFileSync(fullpath, 'utf-8'); + + src = '{% do %}var template = template || {}; template.stack = template.stack || []; template.stack.push(template.self); template.self = ' + JSON.stringify(templateName) + '{% enddo %}\n' + + src + + '\n{% do %}template.self = template.stack.pop();{% enddo %}'; + + return { + src: src, + path: fullpath, + noCache: true + }; + }, + + /* + Nunjucks calls "isRelative" to determine when to call "resolve". + We handle absolute paths ourselves in ".resolve" so we always return true + */ + isRelative: function() { + return true; + }, + + /* + Get original search path containing a template + + @param {String} filepath + @return {String} searchPath + */ + getSearchPath: function(filepath) { + return _.chain(this.searchPaths) + .sortBy(function(s) { + return -s.length; + }) + .find(function(basePath) { + return (filepath && filepath.indexOf(basePath) === 0); + }) + .value(); + }, + + /* + Get template name from a filepath + + @param {String} filepath + @return {String} name + */ + getTemplateName: function(filepath) { + var originalSearchPath = this.getSearchPath(filepath); + return originalSearchPath? path.relative(originalSearchPath, filepath) : null; + }, + + /* + Resolve a template from a current template + + @param {String|null} from + @param {String} to + @return {String|null} + */ + resolve: function(from, to) { + var searchPaths = this.searchPaths; + + // Relative template like "./test.html" + if (isRelative(to) && from) { + return path.resolve(path.dirname(from), to); + } + + // Determine in which search folder we currently are + var originalSearchPath = this.getSearchPath(from); + var originalFilename = this.getTemplateName(from); + + // If we are including same file from a different search path + // Slice the search paths to avoid including from previous ones + if (originalFilename == to) { + var currentIndex = searchPaths.indexOf(originalSearchPath); + searchPaths = searchPaths.slice(currentIndex + 1); + } + + // Absolute template to resolve in root folder + var resultFolder = _.find(searchPaths, function(basePath) { + var p = path.resolve(basePath, to); + + return ( + p.indexOf(basePath) === 0 + && fs.existsSync(p) + ); + }); + if (!resultFolder) return null; + return path.resolve(resultFolder, to); + } +}); + +module.exports = ThemeLoader; |