summaryrefslogtreecommitdiffstats
path: root/lib/output2/website
diff options
context:
space:
mode:
Diffstat (limited to 'lib/output2/website')
-rw-r--r--lib/output2/website/index.js225
-rw-r--r--lib/output2/website/templateEnv.js95
-rw-r--r--lib/output2/website/themeLoader.js127
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;