UNPKG

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