UNPKG

10.3 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.decorators = [];
83
84 this.settings = settings || {};
85 this.themeSettings = themeSettings || {};
86 this.assembler = assembler || {};
87 this.contentServiceContext = {};
88 this.logger = logger;
89
90 helpers.forEach(helper => helper(this));
91 }
92
93 /**
94 * Renders a string with the given context
95 * @param {String} string
96 * @param {Object} context
97 */
98 renderString(string, context) {
99 return this.handlebars.compile(string)(context);
100 }
101
102 loadTheme(paths, acceptLanguage, done) {
103 if (!_.isArray(paths)) {
104 paths = paths ? [paths] : [];
105 }
106
107 Async.parallel([
108 (next) => {
109 this.loadTranslations(acceptLanguage, next);
110 },
111 (next) => {
112 Async.map(paths, this.loadTemplates.bind(this), next);
113 }
114 ], done);
115 }
116
117 /**
118 * Load Partials/Templates
119 * @param {Object} templates
120 * @param {Function} callback
121 */
122 loadTemplates(path, callback) {
123 let processor = this.getTemplateProcessor();
124
125 this.assembler.getTemplates(path, processor, (error, templates) => {
126 if (error) {
127 return callback(error);
128 }
129
130 _.each(templates, (precompiled, path) => {
131 var template;
132 if (!this.handlebars.templates[path]) {
133 eval('template = ' + precompiled);
134 this.handlebars.templates[path] = this.handlebars.template(template);
135 }
136 });
137
138 this.handlebars.partials = this.handlebars.templates;
139
140 callback();
141 });
142 }
143
144 getTemplateProcessor() {
145 return (templates) => {
146 let precompiledTemplates = {};
147
148 _.each(templates,(content, path) => {
149 precompiledTemplates[path] = this.handlebars.precompile(content, handlebarsOptions);
150 });
151
152 return precompiledTemplates;
153 }
154 }
155
156 /**
157 * Load Partials/Templates used for test cases and stencil-cli
158 * @param {Object} templates
159 * @return {Object}
160 */
161 loadTemplatesSync(templates) {
162 _.each(templates,(content, fileName) => {
163 this.handlebars.templates[fileName] = this.handlebars.compile(content, handlebarsOptions);
164 });
165
166 this.handlebars.partials = this.handlebars.templates;
167
168 return this;
169 };
170
171 /**
172 * @param {String} acceptLanguage
173 * @param {Object} translations
174 */
175 loadTranslations(acceptLanguage, callback) {
176 this.assembler.getTranslations((error, translations) => {
177 if (error) {
178 return callback(error);
179 }
180
181 // Make translations available to the helpers
182 this.translator = Translator.create(acceptLanguage, translations, this.logger);
183
184 callback();
185 });
186 };
187
188 /**
189 * Add CDN base url to the relative path
190 * @param {String} path Relative path
191 * @return {String} Url cdn
192 */
193 cdnify(path) {
194 const cdnUrl = this.settings['cdn_url'] || '';
195 const versionId = this.settings['theme_version_id'];
196 const sessionId = this.settings['theme_session_id'];
197 const protocolMatch = /(.*!?:)/;
198
199 if (path instanceof Handlebars.SafeString) {
200 path = path.string;
201 }
202
203 if (!path) {
204 return '';
205 }
206
207 if (/^(?:https?:)?\/\//.test(path)) {
208 return path;
209 }
210
211 if (protocolMatch.test(path)) {
212 var match = path.match(protocolMatch);
213 path = path.slice(match[0].length, path.length);
214
215 if (path[0] === '/') {
216 path = path.slice(1, path.length);
217 }
218
219 if (match[0] === 'webdav:') {
220 return [cdnUrl, 'content', path].join('/');
221 }
222
223 if (this.themeSettings.cdn) {
224 var endpointKey = match[0].substr(0, match[0].length - 1);
225 if (this.themeSettings.cdn.hasOwnProperty(endpointKey)) {
226 if (cdnUrl) {
227 return [this.themeSettings.cdn[endpointKey], path].join('/');
228 }
229
230 return ['/assets/cdn', endpointKey, path].join('/');
231 }
232 }
233
234 if (path[0] !== '/') {
235 path = '/' + path;
236 }
237
238 return path;
239 }
240
241 if (!versionId) {
242 if (path[0] !== '/') {
243 path = '/' + path;
244 }
245
246 return path;
247 }
248
249 if (path[0] === '/') {
250 path = path.slice(1, path.length);
251 }
252
253 if (path.match(/^assets\//)) {
254 path = path.substr(7, path.length);
255 }
256
257 if (sessionId) {
258 return [cdnUrl, 'stencil', versionId, 'e', sessionId, path].join('/');
259 }
260
261 return [cdnUrl, 'stencil', versionId, path].join('/');
262 };
263
264 /**
265 * @param {Function} decorator
266 */
267 addDecorator(decorator) {
268 this.decorators.push(decorator);
269 };
270
271 /**
272 * @param {String} path
273 * @param {Object} context
274 * @return {String}
275 */
276 render(path, context) {
277 let output;
278
279 context = context || {};
280 context.template = path;
281
282 if (this.translator) {
283 context.locale_name = this.translator.getLocale();
284 }
285
286 output = this.handlebars.templates[path](context);
287
288 _.each(this.decorators, function (decorator) {
289 output = decorator(output);
290 });
291
292 return output;
293 };
294
295 /**
296 * Theme rendering logic
297 * @param {String|Array} templatePath
298 * @param {Object} data
299 * @return {String|Object}
300 */
301 renderTheme(templatePath, data) {
302 let html;
303 let output;
304
305 // Is an ajax request?
306 if (data.remote || _.isArray(templatePath)) {
307
308 if (data.remote) {
309 data.context = Object.assign({}, data.context, data.remote_data);
310 }
311
312 // Is render_with ajax request?
313 if (templatePath) {
314 // if multiple render_with
315 if (_.isArray(templatePath)) {
316 // if templatePath is an array ( multiple templates using render_with option)
317 // compile all the template required files into a hash table
318 html = templatePath.reduce((table, file) => {
319 table[file] = this.render(file, data.context);
320 return table;
321 }, {});
322 } else {
323 html = this.render(templatePath, data.context);
324 }
325
326 if (data.remote) {
327 // combine the context & rendered html
328 output = {
329 data: data.remote_data,
330 content: html
331 };
332 } else {
333 output = html;
334 }
335 } else {
336 output = {
337 data: data.remote_data
338 };
339 }
340 } else {
341 output = this.render(templatePath, data.context);
342 }
343
344 return output;
345 }
346
347}
348
349module.exports = Paper;
\No newline at end of file