1 | ;
|
2 |
|
3 | var _ = require('lodash');
|
4 | var Translator = require('./lib/translator');
|
5 | var Path = require('path');
|
6 | var Fs = require('fs');
|
7 | var Handlebars = require('handlebars');
|
8 | var Async = require('async');
|
9 | var helpers = [];
|
10 | var handlebarsOptions = {
|
11 | preventIndent: true
|
12 | };
|
13 |
|
14 | // Load helpers (this only run once)
|
15 | Fs.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 | */
|
73 | class 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 |
|
342 | module.exports = Paper;
|