diff options
Diffstat (limited to 'lib/templating')
-rw-r--r-- | lib/templating/__tests__/conrefsLoader.js | 34 | ||||
-rw-r--r-- | lib/templating/conrefsLoader.js | 72 | ||||
-rw-r--r-- | lib/templating/index.js | 9 | ||||
-rw-r--r-- | lib/templating/listShortcuts.js | 31 | ||||
-rw-r--r-- | lib/templating/postRender.js | 28 | ||||
-rw-r--r-- | lib/templating/render.js | 30 | ||||
-rw-r--r-- | lib/templating/renderFile.js | 38 | ||||
-rw-r--r-- | lib/templating/replaceBlocks.js | 34 | ||||
-rw-r--r-- | lib/templating/replaceShortcuts.js | 37 | ||||
-rw-r--r-- | lib/templating/themesLoader.js | 115 |
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; |