UNPKG

10.4 kBJavaScriptView Raw
1'use strict';
2
3var _ = require('lodash');
4var Translator = require('./lib/translator');
5var Logger = require('./lib/logger');
6var Path = require('path');
7var Fs = require('fs');
8var Handlebars = require('handlebars');
9var Async = require('async');
10var helpers = [];
11var handlebarsOptions = {
12 preventIndent: true
13};
14
15// Load helpers (this only run once)
16Fs.readdirSync(Path.join(__dirname, 'helpers')).forEach(function (file) {
17 helpers.push(require('./helpers/' + file));
18});
19
20/**
21* processor is an optional function to apply during template assembly. The
22* templates parameter is a object where the keys are paths and the values are the
23* raw templates. The function returns an object of the same format, possibly changing
24* the values. We use this to precompile templates within the Paper module.
25*
26* @callback processor
27* @param {Object} templates - Object that contains the gathered templates
28*/
29
30/**
31* getTemplatesCallback is a function to call on completion of Assembler.getTemplates
32*
33* @callback getTemplatesCallback
34* @param {Error} err - Error if it occurred, null otherwise
35* @param {Object} templates - Object that contains the gathered templates, including processing
36*/
37
38/**
39* getTranslationsCallback is a function to call on completion of Assembler.getTranslations
40*
41* @callback getTranslationsCallback
42* @param {Error} err - Error if it occurred, null otherwise
43* @param {Object} translations - Object that contains the translations
44*/
45
46/**
47* Assembler.getTemplates assembles all the templates required to render the given
48* top-level template.
49*
50* @callback assemblerGetTemplates
51* @param {string} path - The path to the templates, relative to the templates directory
52* @param {processor} processor - An optional processor to apply to each template during assembly
53* @param {getTemplatesCallback} callback - Callback when Assembler.getTemplates is done.
54*/
55
56/**
57* Assembler.getTranslations assembles all the translations for the theme.
58*
59* @callback assemblerGetTranslations
60* @param {getTranslationsCallback} callback - Callback when Assembler.getTranslations is done.
61*/
62
63/**
64* Paper constructor. In addition to store settings and theme settings (configuration),
65* paper expects to be passed an assembler to gather all the templates required to render
66* the top level template.
67*
68* @param {Object} settings - Site settings
69* @param {Object} themeSettings - Theme settings (configuration)
70* @param {Object} assembler - Assembler with getTemplates and getTranslations methods.
71* @param {assemblerGetTemplates} assembler.getTemplates - Method to assemble templates
72* @param {assemblerGetTranslations} assembler.getTranslations - Method to assemble translations
73* @param {Object} logger - optional, override the default logger
74*/
75class Paper {
76 constructor(settings, themeSettings, assembler, logger = Logger) {
77 this.handlebars = Handlebars.create();
78
79 this.handlebars.templates = {};
80 this.translator = null;
81 this.inject = {};
82 this.variables = {};
83 this.decorators = [];
84
85 this.settings = settings || {};
86 this.themeSettings = themeSettings || {};
87 this.assembler = assembler || {};
88 this.contentServiceContext = {};
89 this.logger = logger;
90
91 helpers.forEach(helper => helper(this));
92 }
93
94 /**
95 * Renders a string with the given context
96 * @param {String} string
97 * @param {Object} context
98 */
99 renderString(string, context) {
100 return this.handlebars.compile(string)(context);
101 }
102
103 loadTheme(paths, acceptLanguage, done) {
104 if (!_.isArray(paths)) {
105 paths = paths ? [paths] : [];
106 }
107
108 Async.parallel([
109 (next) => {
110 this.loadTranslations(acceptLanguage, next);
111 },
112 (next) => {
113 Async.map(paths, this.loadTemplates.bind(this), next);
114 }
115 ], done);
116 }
117
118 /**
119 * Load Partials/Templates
120 * @param {Object} templates
121 * @param {Function} callback
122 */
123 loadTemplates(path, callback) {
124 let processor = this.getTemplateProcessor();
125
126 this.assembler.getTemplates(path, processor, (error, templates) => {
127 if (error) {
128 return callback(error);
129 }
130
131 _.each(templates, (precompiled, path) => {
132 var template;
133 if (!this.handlebars.templates[path]) {
134 eval('template = ' + precompiled);
135 this.handlebars.templates[path] = this.handlebars.template(template);
136 }
137 });
138
139 this.handlebars.partials = this.handlebars.templates;
140
141 callback();
142 });
143 }
144
145 getTemplateProcessor() {
146 return (templates) => {
147 let precompiledTemplates = {};
148
149 _.each(templates,(content, path) => {
150 precompiledTemplates[path] = this.handlebars.precompile(content, handlebarsOptions);
151 });
152
153 return precompiledTemplates;
154 }
155 }
156
157 /**
158 * Load Partials/Templates used for test cases and stencil-cli
159 * @param {Object} templates
160 * @return {Object}
161 */
162 loadTemplatesSync(templates) {
163 _.each(templates,(content, fileName) => {
164 this.handlebars.templates[fileName] = this.handlebars.compile(content, handlebarsOptions);
165 });
166
167 this.handlebars.partials = this.handlebars.templates;
168
169 return this;
170 };
171
172 /**
173 * @param {String} acceptLanguage
174 * @param {Object} translations
175 */
176 loadTranslations(acceptLanguage, callback) {
177 this.assembler.getTranslations((error, translations) => {
178 if (error) {
179 return callback(error);
180 }
181
182 // Make translations available to the helpers
183 this.translator = Translator.create(acceptLanguage, translations, this.logger);
184
185 callback();
186 });
187 };
188
189 /**
190 * Add CDN base url to the relative path
191 * @param {String} path Relative path
192 * @return {String} Url cdn
193 */
194 cdnify(path) {
195 const cdnUrl = this.settings['cdn_url'] || '';
196 const versionId = this.settings['theme_version_id'];
197 const sessionId = this.settings['theme_session_id'];
198 const protocolMatch = /(.*!?:)/;
199
200 if (path instanceof Handlebars.SafeString) {
201 path = path.string;
202 }
203
204 if (!path) {
205 return '';
206 }
207
208 if (/^(?:https?:)?\/\//.test(path)) {
209 return path;
210 }
211
212 if (protocolMatch.test(path)) {
213 var match = path.match(protocolMatch);
214 path = path.slice(match[0].length, path.length);
215
216 if (path[0] === '/') {
217 path = path.slice(1, path.length);
218 }
219
220 if (match[0] === 'webdav:') {
221 return [cdnUrl, 'content', path].join('/');
222 }
223
224 if (this.themeSettings.cdn) {
225 var endpointKey = match[0].substr(0, match[0].length - 1);
226 if (this.themeSettings.cdn.hasOwnProperty(endpointKey)) {
227 if (cdnUrl) {
228 return [this.themeSettings.cdn[endpointKey], path].join('/');
229 }
230
231 return ['/assets/cdn', endpointKey, path].join('/');
232 }
233 }
234
235 if (path[0] !== '/') {
236 path = '/' + path;
237 }
238
239 return path;
240 }
241
242 if (!versionId) {
243 if (path[0] !== '/') {
244 path = '/' + path;
245 }
246
247 return path;
248 }
249
250 if (path[0] === '/') {
251 path = path.slice(1, path.length);
252 }
253
254 if (path.match(/^assets\//)) {
255 path = path.substr(7, path.length);
256 }
257
258 if (sessionId) {
259 return [cdnUrl, 'stencil', versionId, 'e', sessionId, path].join('/');
260 }
261
262 return [cdnUrl, 'stencil', versionId, path].join('/');
263 };
264
265 /**
266 * @param {Function} decorator
267 */
268 addDecorator(decorator) {
269 this.decorators.push(decorator);
270 };
271
272 /**
273 * @param {String} path
274 * @param {Object} context
275 * @return {String}
276 */
277 render(path, context) {
278 let output;
279
280 context = context || {};
281 context.template = path;
282
283 if (this.translator) {
284 context.locale_name = this.translator.getLocale();
285 }
286
287 output = this.handlebars.templates[path](context);
288
289 _.each(this.decorators, function (decorator) {
290 output = decorator(output);
291 });
292
293 return output;
294 };
295
296 /**
297 * Theme rendering logic
298 * @param {String|Array} templatePath
299 * @param {Object} data
300 * @return {String|Object}
301 */
302 renderTheme(templatePath, data) {
303 let html;
304 let output;
305
306 // Is an ajax request?
307 if (data.remote || _.isArray(templatePath)) {
308
309 if (data.remote) {
310 data.context = Object.assign({}, data.context, data.remote_data);
311 }
312
313 // Is render_with ajax request?
314 if (templatePath) {
315 // if multiple render_with
316 if (_.isArray(templatePath)) {
317 // if templatePath is an array ( multiple templates using render_with option)
318 // compile all the template required files into a hash table
319 html = templatePath.reduce((table, file) => {
320 table[file] = this.render(file, data.context);
321 return table;
322 }, {});
323 } else {
324 html = this.render(templatePath, data.context);
325 }
326
327 if (data.remote) {
328 // combine the context & rendered html
329 output = {
330 data: data.remote_data,
331 content: html
332 };
333 } else {
334 output = html;
335 }
336 } else {
337 output = {
338 data: data.remote_data
339 };
340 }
341 } else {
342 output = this.render(templatePath, data.context);
343 }
344
345 return output;
346 }
347
348}
349
350module.exports = Paper;