@nakedcreativity/cee-ms-node 1.0.121 → 2.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +81 -0
  2. package/lib/draft.d.ts +20 -0
  3. package/lib/draft.js +109 -0
  4. package/lib/draft.js.map +1 -0
  5. package/lib/filters/active.js +1 -2
  6. package/lib/filters/active.js.map +1 -1
  7. package/lib/filters/index.js +5 -1
  8. package/lib/filters/index.js.map +1 -1
  9. package/lib/filters/slugify.js +3 -4
  10. package/lib/filters/slugify.js.map +1 -1
  11. package/lib/index.d.ts +51 -10
  12. package/lib/index.js +262 -323
  13. package/lib/index.js.map +1 -1
  14. package/lib/interfaces.d.ts +55 -16
  15. package/lib/interfaces.js +1 -1
  16. package/lib/interfaces.js.map +1 -1
  17. package/lib/preview-token.d.ts +21 -0
  18. package/lib/preview-token.js +37 -0
  19. package/lib/preview-token.js.map +1 -0
  20. package/lib/state.d.ts +17 -0
  21. package/lib/state.js +44 -0
  22. package/lib/state.js.map +1 -0
  23. package/lib/storage.d.ts +6 -0
  24. package/lib/storage.js +24 -0
  25. package/lib/storage.js.map +1 -0
  26. package/lib/tags/blog.d.ts +5 -2
  27. package/lib/tags/blog.js +25 -41
  28. package/lib/tags/blog.js.map +1 -1
  29. package/lib/tags/breadcrumb.d.ts +2 -1
  30. package/lib/tags/breadcrumb.js +13 -15
  31. package/lib/tags/breadcrumb.js.map +1 -1
  32. package/lib/tags/image.d.ts +2 -0
  33. package/lib/tags/image.js +25 -68
  34. package/lib/tags/image.js.map +1 -1
  35. package/lib/tags/index.js +5 -1
  36. package/lib/tags/index.js.map +1 -1
  37. package/lib/tags/module.d.ts +1 -0
  38. package/lib/tags/module.js +6 -6
  39. package/lib/tags/module.js.map +1 -1
  40. package/lib/tags/navigation.d.ts +5 -2
  41. package/lib/tags/navigation.js +14 -217
  42. package/lib/tags/navigation.js.map +1 -1
  43. package/lib/tags/page.d.ts +5 -2
  44. package/lib/tags/page.js +9 -21
  45. package/lib/tags/page.js.map +1 -1
  46. package/lib/tags/region.d.ts +1 -0
  47. package/lib/tags/region.js +6 -7
  48. package/lib/tags/region.js.map +1 -1
  49. package/package.json +25 -26
  50. package/lib/test-tags.d.ts +0 -3
  51. package/lib/test-tags.js +0 -125
  52. package/lib/test-tags.js.map +0 -1
  53. package/lib/test.d.ts +0 -1
  54. package/lib/test.js +0 -52
  55. package/lib/test.js.map +0 -1
package/lib/index.js CHANGED
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
2
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
3
  if (k2 === undefined) k2 = k;
4
- Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
5
9
  }) : (function(o, m, k, k2) {
6
10
  if (k2 === undefined) k2 = k;
7
11
  o[k2] = m[k];
@@ -11,30 +15,63 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
11
15
  }) : function(o, v) {
12
16
  o["default"] = v;
13
17
  });
14
- var __importStar = (this && this.__importStar) || function (mod) {
15
- if (mod && mod.__esModule) return mod;
16
- var result = {};
17
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18
- __setModuleDefault(result, mod);
19
- return result;
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
36
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
20
37
  };
21
38
  var __importDefault = (this && this.__importDefault) || function (mod) {
22
39
  return (mod && mod.__esModule) ? mod : { "default": mod };
23
40
  };
24
41
  Object.defineProperty(exports, "__esModule", { value: true });
25
- exports.Ceems = void 0;
42
+ exports.Ceems = exports.renderState = exports.verifyPreviewToken = exports.signPreviewToken = void 0;
26
43
  const express_1 = __importDefault(require("express"));
27
44
  const path_1 = __importDefault(require("path"));
28
45
  const fs_1 = __importDefault(require("fs"));
29
- const underscore_1 = __importDefault(require("underscore")); // change to lodash
30
- const liquidjs_1 = require("liquidjs"); // use main package?
46
+ const liquidjs_1 = require("liquidjs");
31
47
  const cheerio = __importStar(require("cheerio"));
32
- const body_parser_1 = __importDefault(require("body-parser"));
33
48
  const tags_1 = require("./tags");
34
49
  const filters_1 = require("./filters");
35
- const aws_sdk_1 = require("aws-sdk");
36
50
  const interfaces_1 = require("./interfaces");
51
+ const storage_1 = require("./storage");
52
+ const state_1 = require("./state");
53
+ const draft_1 = require("./draft");
54
+ __exportStar(require("./interfaces"), exports);
55
+ var preview_token_1 = require("./preview-token");
56
+ Object.defineProperty(exports, "signPreviewToken", { enumerable: true, get: function () { return preview_token_1.signPreviewToken; } });
57
+ Object.defineProperty(exports, "verifyPreviewToken", { enumerable: true, get: function () { return preview_token_1.verifyPreviewToken; } });
58
+ var state_2 = require("./state");
59
+ Object.defineProperty(exports, "renderState", { enumerable: true, get: function () { return state_2.renderState; } });
60
+ /**
61
+ * A Cee.ms site renderer: an Express app that renders published pages, and verified editor drafts, with the
62
+ * site's Liquid templates.
63
+ *
64
+ * One instance serves every request, so nothing about a request is stored on it. Each render carries its own
65
+ * `RenderState` (site, page, edit mode), passed to templates and tags as render-time globals.
66
+ */
37
67
  class Ceems {
68
+ app;
69
+ engine;
70
+ config;
71
+ /** The latest published `site.json`, refreshed on every public request. Never holds draft data. */
72
+ site;
73
+ storage;
74
+ drafts;
38
75
  constructor(config) {
39
76
  this.config = config;
40
77
  // set defaults
@@ -44,367 +81,269 @@ class Ceems {
44
81
  if (!this.config.viewsPath) {
45
82
  this.config.viewsPath = './views/';
46
83
  }
47
- if (!this.config.liquidCache) {
84
+ if (this.config.liquidCache === undefined) {
48
85
  this.config.liquidCache = true;
49
86
  }
50
- if (!this.config.tags) {
51
- this.config.tags = [];
52
- }
53
- if (!this.config.filters) {
54
- this.config.filters = [];
55
- }
56
- this.app = express_1.default();
87
+ this.config.tags = this.config.tags || [];
88
+ this.config.filters = this.config.filters || [];
89
+ this.storage =
90
+ this.config.cacheSource === interfaces_1.CacheSource.S3
91
+ ? (0, storage_1.s3Storage)(this.config.cacheBucket || '', this.config.cacheRegion || process.env.AWS_REGION)
92
+ : (0, storage_1.localStorage)(this.config.cacheFolder || './');
93
+ // One engine for the life of the process. Site data is passed per render, never through these options.
94
+ this.engine = new liquidjs_1.Liquid({
95
+ root: path_1.default.resolve(this.config.viewsPath),
96
+ extname: '.liquid',
97
+ cache: this.config.liquidCache,
98
+ jsTruthy: true,
99
+ globals: { ...(this.config.globals || {}) },
100
+ });
101
+ this.registerTags();
102
+ this.app = (0, express_1.default)();
57
103
  this.app.set('case sensitive routing', true);
58
- // process config and set defaults
59
- // // set up static file hosting
104
+ this.app.engine('liquid', this.engine.express());
105
+ this.app.set('views', this.config.viewsPath);
106
+ this.app.set('view engine', 'liquid');
60
107
  this.app.use(express_1.default.static(this.config.assets));
61
- this.app.use(body_parser_1.default.json({
62
- limit: this.config.parseLimit || '250kb',
63
- }));
64
- this.app.use(body_parser_1.default.urlencoded({
65
- extended: false,
66
- limit: this.config.parseLimit || '250kb',
67
- }));
68
- // pagination and initial path processing
69
- // this.app.use((req: CeemsRequest, res: express.Response, next) => {
70
- // const regex = new RegExp('(.+)(/[0-9]{1,})$');
71
- // this.path = req._parsedUrl.pathname.substring(1);
72
- // if (regex.test(this.path)) {
73
- // // looks like a blog url
74
- // const matches = this.path.match(regex);
75
- // this.path = matches[1];
76
- // this.pagination = parseInt(matches[2].substring(1));
77
- // }
78
- // if (this.path === '') this.path = 'home';
79
- // next();
80
- // });
81
- // this.app.use((req, res, next) => {
82
- // if (this.config.response && this.config.response.headers) {
83
- // _.each(this.config.response.headers, (header) => {
84
- // res.header(header[0], header[1]);
85
- // });
86
- // }
87
- // next();
88
- // });
89
- // load site map
108
+ this.app.use(express_1.default.json({ limit: this.config.parseLimit || '250kb' }));
109
+ this.app.use(express_1.default.urlencoded({ extended: false, limit: this.config.parseLimit || '250kb' }));
110
+ // Drafts are registered before the published-site middleware: they never read or replace `this.site`.
111
+ this.drafts = (0, draft_1.draftServiceFor)(this.config.preview);
112
+ this.app.get('/draft/:pageId', (req, res) => this.handleDraft(req, res));
113
+ // load the published site map for every page request
90
114
  this.app.use((req, res, next) => {
91
115
  this.loadSite().then((site) => {
116
+ // each request renders against the snapshot it started with
117
+ req.ceemsSite = site;
92
118
  next();
93
- });
119
+ }, (error) => next(error));
94
120
  });
95
121
  if (!this.config.customHandler) {
96
122
  this.app.get(/(.*)/, async (req, res) => {
97
- let page = await this.load(req, res, true);
98
- this.render(page, req, res);
123
+ try {
124
+ const page = await this.load(req, res, true);
125
+ await this.render(page, req, res);
126
+ }
127
+ catch (error) {
128
+ if (!(0, state_1.isNotFound)(error))
129
+ console.error('[ceems] load failed', error);
130
+ if (!res.headersSent)
131
+ res.status(404).send('Not found');
132
+ }
99
133
  });
100
134
  }
101
- // init routes
102
- this.app.post(/draft\/(.*)/, async (req, res) => {
103
- await this.postDraft(req, res);
104
- });
105
135
  }
106
- loadSite(draft) {
107
- let sitePromise;
108
- if (!draft) {
109
- // do we use local files or S3 as source?
110
- if (this.config.cacheSource == interfaces_1.CacheSource.S3) {
111
- this.s3 = new aws_sdk_1.S3();
112
- sitePromise = this.s3
113
- .getObject({
114
- Bucket: this.config.cacheBucket,
115
- Key: 'pages/site.json'
116
- })
117
- .promise()
118
- .then((result) => {
119
- return result.Body.toString('utf-8');
120
- });
121
- }
122
- if (this.config.cacheSource == interfaces_1.CacheSource.Local) {
123
- sitePromise = fs_1.default.promises.readFile(this.config.cacheFolder + 'pages/site.json', 'utf-8');
124
- }
136
+ registerTags() {
137
+ (0, tags_1.registerRegionTag)(this.engine);
138
+ (0, tags_1.registerModuleTag)(this.engine);
139
+ (0, tags_1.registerNavigationTag)(this.engine);
140
+ (0, tags_1.registerBlogTag)(this.engine);
141
+ (0, tags_1.registerPageTag)(this.engine, this);
142
+ (0, tags_1.registerImageTag)(this.engine);
143
+ (0, tags_1.registerImageUrlTag)(this.engine);
144
+ (0, tags_1.registerBreadcrumbTag)(this.engine);
145
+ (0, filters_1.registerActiveFilter)(this.engine);
146
+ (0, filters_1.registerSlugifyFilter)(this.engine);
147
+ // custom tags and filters
148
+ for (const tag of this.config.tags || []) {
149
+ this.engine.registerTag(tag.name, tag.register(this));
125
150
  }
126
- else {
127
- sitePromise = new Promise((resolve, reject) => {
128
- resolve(draft);
129
- });
151
+ for (const filter of this.config.filters || []) {
152
+ this.engine.registerFilter(filter.name, filter.filter);
130
153
  }
131
- return sitePromise.then((site) => {
132
- this.site = JSON.parse(site);
133
- this.engine = new liquidjs_1.Liquid({
134
- root: path_1.default.resolve(this.config.viewsPath),
135
- extname: '.liquid',
136
- cache: this.config.liquidCache,
137
- jsTruthy: true,
138
- globals: Object.assign({}, this.site)
139
- });
140
- });
141
154
  }
155
+ /** Loads the published `site.json`, remembers it as `this.site`, and returns it. */
156
+ async loadSite() {
157
+ const site = JSON.parse(await this.storage.read('pages/site.json'));
158
+ this.site = site;
159
+ return site;
160
+ }
161
+ /**
162
+ * Loads the published page for the request's path (or the `404` page), ready for `render`. With
163
+ * `loadPage` false, returns `page` as given (for custom routes that build their own page object).
164
+ */
142
165
  async load(req, res, loadPage, page) {
143
- // to-do: pass in full liquidjs config
144
- // register liquid engine
145
- this.app.engine('liquid', this.engine.express());
146
- this.app.set('views', this.config.viewsPath);
147
- this.app.set('view engine', 'liquid');
166
+ const site = req.ceemsSite || this.site || (await this.loadSite());
148
167
  if (loadPage) {
149
- let path = this.getPath(req);
150
- console.log('there are ', this.site.pages.length, 'pages');
151
- if (this.site.pages[path]) {
152
- page = this.site.pages[path];
153
- if (this.config.cacheSource == interfaces_1.CacheSource.Local) {
154
- page = this.loadLocalFile(this.config.cacheFolder + 'pages/' + page._id + '.json');
155
- }
156
- if (this.config.cacheSource == interfaces_1.CacheSource.S3) {
157
- page = await this.loadS3File('pages/' + page._id + '.json');
158
- }
159
- page.fullpath = this.site.pages[path].url == 'home' ? '' : this.site.pages[path].url;
168
+ const { path, pagination } = this.parsePath(req);
169
+ const entry = site.pages[path];
170
+ if (entry) {
171
+ page = await this.loadPageFile(entry._id);
172
+ page.fullpath = entry.url == 'home' ? '' : entry.url;
160
173
  page.path = page.fullpath.split('/');
161
- if (this.pagination)
162
- page.pagination = this.pagination;
174
+ if (pagination)
175
+ page.pagination = pagination;
163
176
  }
164
- // 404
165
177
  else {
166
- console.log('404 for ', path);
167
178
  res.status(404);
168
- // .send('Sorry, this page does not exist');
169
- let notFound = this.site.pages['404'];
170
- if (this.config.cacheSource == interfaces_1.CacheSource.Local) {
171
- page = this.loadLocalFile(this.config.cacheFolder + 'pages/' + notFound._id + '.json');
172
- }
173
- if (this.config.cacheSource == interfaces_1.CacheSource.S3) {
174
- page = await this.loadS3File('pages/' + notFound._id + '.json');
179
+ const notFound = site.pages['404'];
180
+ if (notFound) {
181
+ page = await this.loadPageFile(notFound._id);
175
182
  }
176
183
  }
177
184
  }
178
- tags_1.registerRegionTag(this.engine);
179
- tags_1.registerModuleTag(this.engine);
180
- tags_1.registerNavigationTag(this.engine, this.site, page);
181
- tags_1.registerBlogTag(this.engine, this.site, page);
182
- tags_1.registerPageTag(this.engine, this, this.site);
183
- tags_1.registerImageTag(this.engine);
184
- tags_1.registerImageUrlTag(this.engine);
185
- filters_1.registerActiveFilter(this.engine);
186
- filters_1.registerSlugifyFilter(this.engine);
187
- tags_1.registerBreadcrumbTag(this.engine, this.site, page);
188
- // register each of our custom tags and filters
189
- this.config.tags.forEach((tag) => {
190
- this.engine.registerTag(tag.name, tag.register(this));
191
- });
192
- this.config.filters.forEach((tag) => {
193
- this.engine.registerFilter(tag.name, tag.filter);
194
- });
195
- return page;
185
+ if (!page) {
186
+ throw new state_1.NotFoundError('No page');
187
+ }
188
+ return (0, state_1.attachState)(page, { site, page, edit: false });
196
189
  }
190
+ /** Reads `pages/<id>.json`. */
191
+ async loadPageFile(id) {
192
+ return JSON.parse(await this.storage.read('pages/' + id + '.json'));
193
+ }
194
+ /** Synchronous local read, kept for custom tags. `filename` is a full path. */
197
195
  loadLocalFile(filename) {
198
196
  return JSON.parse(fs_1.default.readFileSync(filename, 'utf8'));
199
197
  }
200
- loadS3File(filename) {
201
- return this.s3
202
- .getObject({
203
- Bucket: this.config.cacheBucket,
204
- Key: filename,
205
- })
206
- .promise()
207
- .then((result) => {
208
- return JSON.parse(result.Body.toString('utf-8'));
209
- });
198
+ /** Reads and parses a published JSON file, kept for custom tags. `filename` is relative to the bucket. */
199
+ async loadS3File(filename) {
200
+ return JSON.parse(await this.storage.read(filename));
210
201
  }
211
- getPath(req) {
202
+ /** The site path for a request (`home` for `/`), and the blog page number if the URL ends in one. */
203
+ parsePath(req) {
212
204
  if (req.overriddenPath) {
213
- return req.overriddenPath;
205
+ return { path: req.overriddenPath, pagination: null };
214
206
  }
215
207
  const regex = new RegExp('(.+)(/[0-9]{1,})$');
216
- let path = req._parsedUrl.pathname.substring(1);
217
- // fix the 1000 bug
218
- this.pagination = null;
219
- if (regex.test(path)) {
208
+ let path = (req._parsedUrl?.pathname || req.path).substring(1);
209
+ let pagination = null;
210
+ const matches = path.match(regex);
211
+ if (matches) {
220
212
  // looks like a blog url
221
- const matches = path.match(regex);
222
213
  path = matches[1];
223
- this.pagination = parseInt(matches[2].substring(1));
214
+ pagination = parseInt(matches[2].substring(1));
224
215
  }
225
216
  if (path === '')
226
217
  path = 'home';
227
- return path;
218
+ return { path, pagination };
219
+ }
220
+ /** @deprecated Use `parsePath`. */
221
+ getPath(req) {
222
+ return this.parsePath(req).path;
223
+ }
224
+ /** The Liquid globals for one render: the site data, static globals, and the render state. */
225
+ globalsFor(state) {
226
+ return { ...state.site, ...this.engine.options.globals, [state_1.RENDER_STATE]: state };
227
+ }
228
+ async renderModule(module, state) {
229
+ const scope = {
230
+ ...module.content,
231
+ attributes: { _id: module._id, region: module.region, order: module.order },
232
+ page: state.page,
233
+ site: state.site.site,
234
+ pages: state.site.pages,
235
+ navigation: state.site.navigation,
236
+ };
237
+ let html = await this.engine.renderFile(module.type, scope, { globals: this.globalsFor(state) });
238
+ if (state.edit) {
239
+ const $ = cheerio.load(html, null, false);
240
+ $.root().children().attr('data-module-type', module.type);
241
+ if (module._id) {
242
+ $.root().children().attr('data-module-id', String(module._id));
243
+ }
244
+ html = $.html();
245
+ }
246
+ return html;
247
+ }
248
+ async renderRegion(key, modules, state) {
249
+ const html = (await Promise.all(modules.map((module) => this.renderModule(module, state)))).join('');
250
+ // In drafts, comments mark each region so the editor can replace one without reloading the page.
251
+ return state.edit ? `<!--ceems:region:${key}-->${html}<!--/ceems:region:${key}-->` : html;
228
252
  }
229
- render(page, req, res, send = true) {
253
+ /** Renders a full page with its layout template. */
254
+ async renderPage(state) {
255
+ const page = state.page;
230
256
  if (this.config.whitelistedCookies) {
231
257
  page.cookies = {};
232
- underscore_1.default.each(this.config.whitelistedCookies, (cookieName) => {
233
- // if (req.cookies && req.cookies[cookieName]) {
234
- // page.cookies[cookieName] = req.cookies[cookieName];
235
- // }
236
- });
237
258
  }
238
- let renderedRegions = {};
239
- let renderedModules = {};
240
- let jobs = [];
241
- // load page data
242
- underscore_1.default.each(page.regions, (region, key) => {
243
- renderedRegions[key] = [];
244
- underscore_1.default.each(region.modules, (module, index) => {
245
- let moduleTemplate = fs_1.default.readFileSync(this.config.viewsPath + module.type + '.liquid', 'utf8');
246
- let moduleContent = Object.assign(Object.assign({}, module.content), { attributes: {
247
- _id: module._id,
248
- region: module.region,
249
- order: module.order
250
- } });
251
- jobs.push(new Promise((resolve, reject) => {
252
- // to-do: we need to give module access to entire page object, and therefore the 'sitemap' and other site information
253
- var scope = Object.assign({}, moduleContent);
254
- scope.page = page;
255
- //console.log('inside module render', scope.page)
256
- if (this.site) {
257
- scope.site = this.site.site;
258
- scope.pages = this.site.pages;
259
- scope.navigation = this.site.navigation;
260
- }
261
- this.engine
262
- .parseAndRender(moduleTemplate, scope)
263
- .then((html) => {
264
- if (req.query.edit != null) {
265
- var $ = cheerio.load(html);
266
- $.root().children().attr('data-module-type', module.type);
267
- if (module._id) {
268
- $.root().children().attr('data-module-id', module._id);
269
- }
270
- html = $.html();
271
- }
272
- //if (region.containers) {
273
- //renderedModules[module._id] = html;
274
- //} else {
275
- renderedRegions[key][index] = html;
276
- //}
277
- return resolve(true);
278
- })
279
- .catch((error) => {
280
- reject(error);
281
- });
282
- }));
283
- }, this);
284
- }, this);
285
- // loop through each module not in a region and generate HTML
286
- underscore_1.default.each(page.modules, (module, key) => {
287
- let moduleTemplate = fs_1.default.readFileSync(this.config.viewsPath + module.type + '.liquid', 'utf8');
288
- let moduleContent = Object.assign(Object.assign({}, module.content), { attributes: {
289
- _id: module._id,
290
- region: module.region,
291
- order: module.order
292
- } });
293
- jobs.push(new Promise((resolve, reject) => {
294
- // we need to give module access to entire page object, and therefore the 'sitemap' and other site information
295
- var scope = Object.assign({}, moduleContent);
296
- // DO WE NEED TO DO THIS?
297
- scope.page = page;
298
- scope.site = this.site.site;
299
- scope.pages = this.site.pages;
300
- this.engine
301
- .parseAndRender(moduleTemplate, scope)
302
- .then(function (html) {
303
- //console.log(html)
304
- if (req.query.edit != null) {
305
- var $ = cheerio.load(html);
306
- $.root().children().attr('data-module-type', module.type);
307
- $.root().children().attr('data-module-id', module._id);
308
- //console.log($.root().children())
309
- html = $.html();
310
- }
311
- page.modules[key] = html;
312
- return resolve(true);
313
- })
314
- .catch((error) => {
315
- reject(error);
316
- });
317
- }));
318
- }, this);
319
- return Promise.all(jobs)
320
- .then((html) => {
321
- let finalPage = {
322
- regions: {},
323
- navigation: {},
324
- page: page,
325
- };
326
- underscore_1.default.each(renderedRegions, (region, key) => {
327
- finalPage.regions[key] = '';
328
- underscore_1.default.each(region, function (html) {
329
- // items in rendered columns to place
330
- if (Object.keys(renderedModules).length > 0) {
331
- underscore_1.default.each(renderedModules, (moduleHtml, key) => {
332
- html = html.replace(new RegExp('{%\\s' + key + '\\s%}', 'g'), moduleHtml);
333
- });
334
- }
335
- finalPage.regions[key] += html;
336
- });
337
- });
338
- let scope = finalPage;
339
- if (this.site && this.site.navigation)
340
- scope.navigation = this.site.navigation;
341
- //console.log('final page render')
342
- //console.log(scope)
343
- return new Promise((resolve, reject) => {
344
- res.render(page.template, scope, (err, html) => {
345
- if (err) {
346
- reject(err);
347
- }
348
- resolve(html);
349
- });
350
- });
351
- }).then((html) => {
352
- if (req.query.edit != null) {
353
- var $ = cheerio.load(html);
354
- $('body').addClass('cee-ms-edit');
355
- $('body').append('<script type="text/javascript" src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>');
356
- $('body').append('<script type="text/javascript" src="https://cdn.cee.ms/site.js"></script>');
357
- html = $.html();
358
- }
359
- // res.header('X-FRAME-OPTIONS', 'allow-from http://localhost:3002');
360
- if (send) {
361
- res.send(html);
362
- }
363
- else {
364
- return html;
365
- }
366
- })
367
- .catch((error) => {
368
- res.status(500).send(error.message);
369
- return;
370
- });
259
+ const regions = {};
260
+ await Promise.all(Object.entries(page.regions || {}).map(async ([key, region]) => {
261
+ regions[key] = await this.renderRegion(key, region.modules || [], state);
262
+ }));
263
+ // modules not in a region, output with `{% module name %}`
264
+ const modules = {};
265
+ await Promise.all(Object.entries(page.modules || {}).map(async ([key, module]) => {
266
+ modules[key] = typeof module === 'string' ? module : await this.renderModule(module, state);
267
+ // legacy templates may read the rendered HTML from the page itself
268
+ page.modules[key] = modules[key];
269
+ }));
270
+ const scope = { regions, modules, navigation: state.site.navigation || {}, page };
271
+ let html = await this.engine.renderFile(page.template, scope, { globals: this.globalsFor(state) });
272
+ if (state.edit && this.drafts) {
273
+ html = this.drafts.injectBridge(html);
274
+ }
275
+ return html;
371
276
  }
372
- async postDraft(req, res) {
373
- // validate authorisation JWT here
374
- let page;
375
- req.query.edit = true;
376
- if (typeof req.body.page === 'string') {
377
- page = JSON.parse(decodeURIComponent(req.body.page));
277
+ /**
278
+ * Renders a page from `load`. Sends the HTML, or returns it when `send` is false. On failure, sends a
279
+ * generic error (404 for a blog page past the last article) and resolves to undefined.
280
+ */
281
+ async render(page, req, res, send = true) {
282
+ const state = (0, state_1.stateOf)(page) || { site: this.site || (await this.loadSite()), page, edit: false };
283
+ try {
284
+ const html = await this.renderPage(state);
285
+ if (!send)
286
+ return html;
287
+ res.send(html);
378
288
  }
379
- else {
380
- page = req.body.page;
289
+ catch (error) {
290
+ const status = (0, state_1.isNotFound)(error) ? 404 : 500;
291
+ if (status === 500)
292
+ console.error('[ceems] render failed', error);
293
+ if (!res.headersSent)
294
+ res.status(status).send(status === 404 ? 'Not found' : 'Something went wrong');
381
295
  }
382
- // if this was sent by the iframe, handle it differently
383
- if (req.query.iframe != null) {
384
- this.site = JSON.parse(decodeURIComponent(req.body.site));
296
+ }
297
+ /**
298
+ * `GET /draft/:pageId?token=…[&module=<id>|&region=<key>]`: a verified editor draft, as a full page, one
299
+ * module, or one region. The token is checked here, then used to fetch the draft from the Cee.ms API.
300
+ */
301
+ async handleDraft(req, res) {
302
+ if (!this.drafts) {
303
+ res.status(404).send('Not found');
304
+ return;
385
305
  }
386
- else {
387
- this.site = req.body.site;
306
+ const draft = await this.drafts.fetch(req.params.pageId, String(req.query.token || ''));
307
+ if (!draft.ok) {
308
+ this.drafts.setHeaders(res);
309
+ res.status(draft.status).send(draft.status === 401 ? 'Unauthorized' : 'Not available');
310
+ return;
388
311
  }
389
- let path;
390
- // find current page fullpath from site pages using id
391
- Object.keys(this.site.pages).forEach(fullpath => {
392
- if (this.site.pages[fullpath]._id == page._id) {
393
- path = fullpath;
312
+ const { page, site } = draft;
313
+ // find the page's full path from the site map
314
+ const entry = Object.values(site.pages || {}).find((p) => p._id == page._id);
315
+ page.fullpath = entry ? (entry.url == 'home' ? '' : entry.url) : page.handle;
316
+ page.path = page.fullpath.split('/');
317
+ const state = { site, page, edit: true };
318
+ try {
319
+ let html;
320
+ if (typeof req.query.module === 'string') {
321
+ const module = Object.values(page.regions || {})
322
+ .flatMap((region) => region.modules || [])
323
+ .find((m) => String(m._id) === req.query.module);
324
+ if (!module)
325
+ throw new state_1.NotFoundError('No such module');
326
+ html = await this.renderModule(module, state);
394
327
  }
395
- });
396
- if (path && this.site.pages[path]) {
397
- page.fullpath = this.site.pages[path].url == 'home' ? '' : this.site.pages[path].url;
328
+ else if (typeof req.query.region === 'string') {
329
+ const region = page.regions?.[req.query.region];
330
+ if (!region)
331
+ throw new state_1.NotFoundError('No such region');
332
+ html = await this.renderRegion(req.query.region, region.modules || [], state);
333
+ }
334
+ else {
335
+ html = await this.renderPage(state);
336
+ }
337
+ this.drafts.setHeaders(res);
338
+ res.type('html').send(html);
398
339
  }
399
- else {
400
- page.fullpath = page.handle;
340
+ catch (error) {
341
+ const status = (0, state_1.isNotFound)(error) ? 404 : 500;
342
+ if (status === 500)
343
+ console.error('[ceems] draft render failed', error);
344
+ this.drafts.setHeaders(res);
345
+ res.status(status).send(status === 404 ? 'Not found' : 'Something went wrong');
401
346
  }
402
- page.path = page.fullpath.split('/');
403
- await this.load(req, res);
404
- // re-register these tags with the page object
405
- tags_1.registerNavigationTag(this.engine, this.site, page);
406
- tags_1.registerBlogTag(this.engine, this.site, page);
407
- this.render(page, req, res);
408
347
  }
409
348
  }
410
349
  exports.Ceems = Ceems;