UNPKG

apostrophe

Version:

Apostrophe is a user-friendly content management system. You'll need more than this core module. See apostrophenow.org to get started.

328 lines (278 loc) • 12.1 kB
var nunjucks = require('nunjucks'); var moment = require('moment'); var qs = require('qs'); var he = require('he'); /** * templates * @augments Augments the apos object with facilities for rendering Nunjucks templates * with access to appropriate data. * @see aposLocals */ var _ = require('lodash'); var async = require('async'); module.exports = function(self) { // Render the specified `template` and send the result via the specified `res` object. The properties of the // `info` object are made available to Nunjucks template code. A wrapper for `apos.partial`. self.render = function(req, res, template, info) { return res.send(self.partial(req, template, info)); }; // Load and render a Nunjucks template by the specified name and give it the // specified data. All of the Apostrophe helpers are available as // aposArea, etc. from the template. You can also render another partial // from within your template by calling `{{ partial('name') }}`. You can pass a // full path for 'name' otherwise it is assumed to be relative to the first // directory on the `dirs` list that contains a matching file. When you call // `partial` from another partial, the dirs list is always the same list given // to the original `partial` call. // // The `views` folder of the Apostrophe module is always implicitly included in // the `dirs` list as the last entry. Earlier entries beat later ones, allowing // for easy overrides. // // The .html extension is assumed if no extension is present. self.partial = function(req, name, data, dirs) { if(typeof req === 'string'){ // in case we didn't get any req object dirs = data; data = name; name = req; req = null; } else if(typeof req === 'object'){ self.initI18nLocal(req); } if (!data) { data = {}; } data.partial = function(name, data) { return self.partial(req, name, data, dirs); }; // Make sure the apos-specific locals are visible to partials too. // If we import ALL the locals we'll point at the wrong views directory // and generally require the developer to worry about not breaking // our partials, which ought to be a black box they can ignore. _.defaults(data, self._aposLocals); var finalName = name; if (!finalName.match(/\.\w+$/)) { finalName += '.html'; } return self.getNunjucksEnv(dirs).getTemplate(finalName).render(data); }; // Render the Nunjucks template in the provided string and give it the // specified data. All of the Apostrophe helpers are available as // aposArea, etc. from the template. You can also render another partial // from within your template by calling `{{ partial('name') }}`. The // `views` folder of the Apostrophe module is always implicitly included in // the `dirs` list as the last entry. Earlier entries beat later ones, allowing // for easy overrides. // // This is mostly used by the mailMixin to render subject lines. self.partialString = function(s, data, dirs) { if (!data) { data = {}; } if (typeof(data.partial) === 'undefined') { data.partial = self.partial; } // Make sure the apos-specific locals are visible to partials too. // If we import ALL the locals we'll point at the wrong views directory // and generally require the developer to worry about not breaking // our partials, which ought to be a black box they can ignore. _.defaults(data, self._aposLocals); return self.getNunjucksEnv(dirs).renderString(s, data); }; var nunjucksEnvs = {}; /** * Get a nunjucks environment in which "include," "extends," etc. search the * specified directories. You may specify a single directory or skip the * parameter. The views folder of the Apostrophe module is always the * last directory searched and you do not need to add it explicitly. * USUALLY YOU SHOULD USE `apos.partial` which invokes this method automatically. * Note you can specify search directories when calling `apos.partial`. * @param {Array} dirs * @return {Object} a nunjucks environment */ self.getNunjucksEnv = function(dirs) { if (!dirs) { dirs = []; } if (!Array.isArray(dirs)) { dirs = [ dirs ]; } dirs = dirs.concat(self.options.partialPaths || []); // The apostrophe module's views directory is always the last // thing tried, so that you can extend the widgetEditor template, etc. dirs = dirs.concat([ __dirname + '/../views' ]); var dirsKey = dirs.join(':'); if (!nunjucksEnvs[dirsKey]) { nunjucksEnvs[dirsKey] = self.newNunjucksEnv(dirs); } return nunjucksEnvs[dirsKey]; }; // Stringify the data as JSON, then escape any sequences that would // cause a <script> tag to end prematurely if the JSON is embedded in it. self.jsonForHtml = function(data) { data = JSON.stringify(data); // , null, ' '); data = data.replace(/<\!\-\-/g, '<\\!--'); data = data.replace(/<\/script\>/gi, '<\\/script>'); return data; }; /** * Create a new nunjucks environment in which the specified directories are * searched for includes, etc. USUALLY YOU SHOULD USE getNunjucksEnv INSTEAD to avoid * creating environments over and over for the same search path, which is inefficient. * @param {Array} dirs * @return {Object} A nunjucks environment */ self.newNunjucksEnv = function(dirs) { var NunjucksLoader = require('./nunjucksLoader.js'); var loader = new NunjucksLoader(dirs, undefined, self); var nunjucksEnv = new nunjucks.Environment(loader); nunjucksEnv.addFilter('date', function(date, format) { // Nunjucks is generally highly tolerant of bad // or missing data. Continue this tradition by not // crashing if date is null. -Tom if (!date) { return ''; } var s = moment(date).format(format); return s; }); nunjucksEnv.addFilter('query', function(data) { return qs.stringify(data); }); nunjucksEnv.addFilter('json', function(data) { return self.jsonForHtml(data); }); nunjucksEnv.addFilter('qs', function(data) { return qs.stringify(data); }); // See apos.build nunjucksEnv.addFilter('build', self.build); nunjucksEnv.addFilter('stripTags', function(data) { return data.replace(/(<([^>]+)>)/ig,""); }); nunjucksEnv.addFilter('nlbr', function(data) { if ((data === null) || (data === undefined)) { // don't crash, nunjucks tolerates nulls return ''; } data = self.globalReplace(data.toString(), "\n", "<br />\n"); return data; }); nunjucksEnv.addFilter('css', function(data) { return self.cssName(data); }); nunjucksEnv.addFilter('truncate', function(data, limit) { return self.truncatePlaintext(data, limit); }); // Output "data" as JSON, escaped to be safe in an // HTML attribute. By default it is escaped to be // included in an attribute quoted with double-quotes, // so all double-quotes in the output must be escaped. // If you quote your attribute with single-quotes // and pass { single: true } to this filter, // single-quotes in the output are escaped instead, // which uses dramatically less space and produces // more readable attributes. // // EXCEPTION: if the data is not an object or array, // it is output literally as a string. This takes // advantage of jQuery .data()'s ability to treat // data attributes that "smell like" objects and arrays // as such and take the rest literally. nunjucksEnv.addFilter('jsonAttribute', function(data, options) { if (typeof(data) === 'object') { return self.escapeHtml(JSON.stringify(data), options); } else { // Make it a string for sure data += ''; return self.escapeHtml(data, options); } }); nunjucksEnv.addFilter('prune', function(data) { // Remove everything that is the result of a join or // other dynamic load, leaving only what is part of // the essential database representation return self.clonePermanent(data); }); if (self.options.configureNunjucks) { self.options.configureNunjucks(nunjucksEnv); } return nunjucksEnv; }; // YOU DON'T WANT TO CALL THIS DIRECTLY. Use the assets mixin and call // renderDecorated on your own module. If you call this directly you likely won't // have the right javascript and assets in your page, except in certain special // cases like rendering a login page that is guaranteed not to need them. // // Decorate the contents of args.content as a complete webpage. If args.refreshing is // true, return just that content, as we're performing an AJAX refresh of the main // content area. If args.refreshing is not true, return it as a completely // new page (CSS, JS, head, body...) wrapped in the outerLayout template. This is // made available to allow developers to render other content the same way // Apostrophe pages are rendered. For instance, it's useful for a local // login page template, a site reorganization screen or anything else that // is a poor fit for a page template or a javascript modal. // // This may go away when nunjucks gets conditional extends. // // As a workaround for the lack of conditional extends in nunjucks the following // special strings are pulled out of args.content and passed to the outer layout: // // <!-- APOS-BODY-CLASS class names here --> // <!-- APOS-TITLE title here --> // <!-- APOS-EXTRA-HEAD extra head element material here --> // <!-- APOS-SEO-DESCRIPTION meta description here --> // // This is a silly hack. // // If safe mode is not set via args.safeMode, raw HTML widget content // is decoded and inserted directly, otherwise it remains in escaped form // with certain marker comments. // // This is a silly hack too, but it might be the only one. self.decoratePageContent = function(args, req) { // On an AJAX refresh of the main content area only, just send the // main content area. The rest of the time, render the outerLayout and // pass the main content to it if (args.refreshing) { return args.content; } else { // This is a bit of a nasty workaround: we need to communicate a few things // to the outer layout, and since it must run as a separate invocation of // nunjucks there's no great way to get them there. // [\s\S] is like . but matches newlines too. Great workaround for the lack // of a /s modifier in JavaScript // http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work var match = args.content.match(/<\!\-\- APOS\-BODY\-CLASS ([\s\S]*?) \-\-\>/); if (match) { args.bodyClass = match[1]; } match = args.content.match(/<\!\-\- APOS\-TITLE ([\s\S]*?) \-\-\>/); if (match) { args.title = match[1]; } match = args.content.match(/<\!\-\- APOS\-EXTRA\-HEAD ([\s\S]*?) \-\-\>/); if (match) { args.extraHead = match[1]; } match = args.content.match(/<\!\-\- APOS\-SEO\-DESCRIPTION ([\s\S]*?) \-\-\>/); if (match) { args.seoDescription = match[1]; } // Allow raw HTML slots on a true page update, without the risk // of document.write blowing up a page during a partial update. // This is pretty nasty too, keep thinking about alternatives. if (!args.safeMode) { args.content = args.content.replace(/<\!\-\- APOS\-RAW\-HTML\-BEFORE \-\-\>[\s\S]*?<\!\-\- APOS\-RAW\-HTML\-START \-\-\>([\s\S]*?)<\!\-\- APOS\-RAW\-HTML\-END \-\-\>[\s\S]*?<\!\-\- APOS\-RAW\-HTML\-AFTER \-\-\>/g, function(all, code) { return he.decode(code); }); } if (typeof(self.options.outerLayout) === 'function') { return self.options.outerLayout(args); } else { return self.partial(req, self.options.outerLayout || 'outerLayout', args); } } }; };