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