summaryrefslogtreecommitdiffstats
path: root/lib/templating
diff options
context:
space:
mode:
Diffstat (limited to 'lib/templating')
-rw-r--r--lib/templating/__tests__/conrefsLoader.js34
-rw-r--r--lib/templating/conrefsLoader.js72
-rw-r--r--lib/templating/index.js9
-rw-r--r--lib/templating/listShortcuts.js31
-rw-r--r--lib/templating/postRender.js28
-rw-r--r--lib/templating/render.js30
-rw-r--r--lib/templating/renderFile.js38
-rw-r--r--lib/templating/replaceBlocks.js34
-rw-r--r--lib/templating/replaceShortcuts.js37
-rw-r--r--lib/templating/themesLoader.js115
10 files changed, 428 insertions, 0 deletions
diff --git a/lib/templating/__tests__/conrefsLoader.js b/lib/templating/__tests__/conrefsLoader.js
new file mode 100644
index 0000000..3480a48
--- /dev/null
+++ b/lib/templating/__tests__/conrefsLoader.js
@@ -0,0 +1,34 @@
+var TemplateEngine = require('../../models/templateEngine');
+var renderTemplate = require('../render');
+
+describe('ConrefsLoader', function() {
+ var ConrefsLoader = require('../conrefsLoader');
+
+ var engine = TemplateEngine({
+ loader: new ConrefsLoader(__dirname)
+ });
+
+ 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');
+ });
+ });
+
+ 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');
+ });
+ });
+
+ 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');
+ });
+ });
+ });
+});
+
diff --git a/lib/templating/conrefsLoader.js b/lib/templating/conrefsLoader.js
new file mode 100644
index 0000000..c3e5048
--- /dev/null
+++ b/lib/templating/conrefsLoader.js
@@ -0,0 +1,72 @@
+var path = require('path');
+var nunjucks = require('nunjucks');
+
+var fs = require('../utils/fs');
+var Git = require('../utils/git');
+var LocationUtils = require('../utils/location');
+var PathUtils = require('../utils/path');
+
+
+/**
+ Template loader resolving both:
+ - relative url ("./test.md")
+ - absolute url ("/test.md")
+ - git url ("")
+*/
+var ConrefsLoader = nunjucks.Loader.extend({
+ async: true,
+
+ init: function(rootFolder, logger) {
+ this.rootFolder = rootFolder;
+ this.logger = logger;
+ this.git = new Git();
+ },
+
+ getSource: function(sourceURL, callback) {
+ var that = this;
+
+ this.git.resolve(sourceURL)
+ .then(function(filepath) {
+ // Is local file
+ if (!filepath) {
+ filepath = path.resolve(sourceURL);
+ } else {
+ if (that.logger) that.logger.debug.ln('resolve from git', sourceURL, 'to', filepath);
+ }
+
+ // Read file from absolute path
+ return fs.readFile(filepath)
+ .then(function(source) {
+ return {
+ src: source.toString('utf8'),
+ path: filepath
+ };
+ });
+ })
+ .nodeify(callback);
+ },
+
+ resolve: function(from, to) {
+ // If origin is in the book, we enforce result file to be in the book
+ if (PathUtils.isInRoot(this.rootFolder, from)) {
+ var href = LocationUtils.toAbsolute(to, path.dirname(from), '');
+ return PathUtils.resolveInRoot(this.rootFolder, href);
+ }
+
+ // If origin is in a git repository, we resolve file in the git repository
+ var gitRoot = this.git.resolveRoot(from);
+ if (gitRoot) {
+ return PathUtils.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'
+ isRelative: function(filename) {
+ return LocationUtils.isRelative(filename);
+ }
+});
+
+module.exports = ConrefsLoader;
diff --git a/lib/templating/index.js b/lib/templating/index.js
new file mode 100644
index 0000000..a33965d
--- /dev/null
+++ b/lib/templating/index.js
@@ -0,0 +1,9 @@
+
+module.exports = {
+ render: require('./render'),
+ renderFile: require('./renderFile'),
+ postRender: require('./postRender'),
+
+ ConrefsLoader: require('./conrefsLoader'),
+ ThemesLoader: require('./themesLoader')
+};
diff --git a/lib/templating/listShortcuts.js b/lib/templating/listShortcuts.js
new file mode 100644
index 0000000..8f2388b
--- /dev/null
+++ b/lib/templating/listShortcuts.js
@@ -0,0 +1,31 @@
+var Immutable = require('immutable');
+var parsers = require('../parsers');
+
+/**
+ Return a list of all shortcuts that can apply
+ to a file for a TemplatEngine
+
+ @param {TemplateEngine} engine
+ @param {String} filePath
+ @return {List<Shortcut>}
+*/
+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);
+ });
+ })
+ .flatten(1);
+}
+
+module.exports = listShortcuts;
diff --git a/lib/templating/postRender.js b/lib/templating/postRender.js
new file mode 100644
index 0000000..6928e82
--- /dev/null
+++ b/lib/templating/postRender.js
@@ -0,0 +1,28 @@
+var Promise = require('../utils/promise');
+var replaceBlocks = require('./replaceBlocks');
+
+/**
+ Post render a template:
+ - Execute "post" for blocks
+ - Replace block content
+
+ @param {TemplateEngine} engine
+ @param {String} content
+ @return {Promise<String>}
+*/
+function postRender(engine, content) {
+ var result = replaceBlocks(content);
+
+ return Promise.forEach(result.blocks, function(blockType) {
+ var block = engine.getBlock();
+ var post = block.getPost();
+ if (!post) {
+ return;
+ }
+
+ return post();
+ })
+ .thenResolve(result.content);
+}
+
+module.exports = postRender;
diff --git a/lib/templating/render.js b/lib/templating/render.js
new file mode 100644
index 0000000..bf21cfe
--- /dev/null
+++ b/lib/templating/render.js
@@ -0,0 +1,30 @@
+var Promise = require('../utils/promise');
+
+var replaceShortcuts = require('./replaceShortcuts');
+
+/**
+ Render a template
+
+ @param {TemplateEngine} engine
+ @param {String} filePath
+ @param {String} content
+ @param {Object} context
+ @return {Promise<String>}
+*/
+function renderTemplate(engine, filePath, content, context) {
+ context = context || {};
+ var env = engine.toNunjucks();
+
+ content = replaceShortcuts(engine, filePath, content);
+
+ return Promise.nfcall(
+ env.renderString.bind(env),
+ content,
+ context,
+ {
+ path: filePath
+ }
+ );
+}
+
+module.exports = renderTemplate;
diff --git a/lib/templating/renderFile.js b/lib/templating/renderFile.js
new file mode 100644
index 0000000..9b74e5b
--- /dev/null
+++ b/lib/templating/renderFile.js
@@ -0,0 +1,38 @@
+var Promise = require('../utils/promise');
+var error = require('../utils/error');
+var render = require('./render');
+
+/**
+ Render a template
+
+ @param {TemplateEngine} engine
+ @param {String} filePath
+ @param {Object} context
+ @return {Promise<String>}
+*/
+function renderTemplateFile(engine, filePath, context) {
+ var loader = engine.getLoader();
+
+ return Promise()
+ .then(function() {
+ if (!loader.async) {
+ return loader.getSource(filePath);
+ }
+
+ var deferred = Promise.defer();
+ loader.getSource(filePath, deferred.makeNodeResolver());
+ return deferred.promise;
+ })
+ .then(function(result) {
+ if (!result) {
+ throw error.TemplateError(new Error('Not found'), {
+ filename: filePath
+ });
+ }
+
+ return render(engine, result.path, result.src, context);
+ });
+
+}
+
+module.exports = renderTemplateFile;
diff --git a/lib/templating/replaceBlocks.js b/lib/templating/replaceBlocks.js
new file mode 100644
index 0000000..4b1c37f
--- /dev/null
+++ b/lib/templating/replaceBlocks.js
@@ -0,0 +1,34 @@
+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;
diff --git a/lib/templating/replaceShortcuts.js b/lib/templating/replaceShortcuts.js
new file mode 100644
index 0000000..f6a51cb
--- /dev/null
+++ b/lib/templating/replaceShortcuts.js
@@ -0,0 +1,37 @@
+var escapeStringRegexp = require('escape-string-regexp');
+var listShortcuts = require('./listShortcuts');
+
+/*
+ Apply a shortcut of block to a template
+ @param {String} content
+ @param {Shortcut} shortcut
+ @return {String}
+*/
+function applyShortcut(content, shortcut) {
+ var tags = shortcut.get('tag');
+ var start = shortcut.get('start');
+ var end = shortcut.get('end');
+
+ var regex = new RegExp(
+ escapeStringRegexp(start) + '([\\s\\S]*?[^\\$])' + escapeStringRegexp(end),
+ 'g'
+ );
+ return content.replace(regex, function(all, match) {
+ return '{% ' + tags.start + ' %}' + match + '{% ' + tags.end + ' %}';
+ });
+}
+
+/**
+ Replace shortcuts from blocks in a string
+
+ @param {TemplateEngine} engine
+ @param {String} filePath
+ @param {String} content
+ @return {String}
+*/
+function replaceShortcuts(engine, filePath, content) {
+ var shortcuts = listShortcuts(engine, filePath);
+ return shortcuts.reduce(applyShortcut, content);
+}
+
+module.exports = replaceShortcuts;
diff --git a/lib/templating/themesLoader.js b/lib/templating/themesLoader.js
new file mode 100644
index 0000000..69c3879
--- /dev/null
+++ b/lib/templating/themesLoader.js
@@ -0,0 +1,115 @@
+var Immutable = require('immutable');
+var nunjucks = require('nunjucks');
+var fs = require('fs');
+var path = require('path');
+
+var PathUtils = require('../utils/path');
+
+
+var ThemesLoader = nunjucks.Loader.extend({
+ init: function(searchPaths) {
+ this.searchPaths = Immutable.List(searchPaths)
+ .map(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 this.searchPaths
+ .sortBy(function(s) {
+ return -s.length;
+ })
+ .find(function(basePath) {
+ return (filepath && filepath.indexOf(basePath) === 0);
+ });
+ },
+
+ /*
+ 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 (PathUtils.isPureRelative(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 = searchPaths.find(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 = ThemesLoader;