@nakedcreativity/cee-ms-node 1.0.122 → 2.0.0-beta.1

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 (58) hide show
  1. package/README.md +81 -0
  2. package/lib/draft.d.ts +20 -0
  3. package/lib/draft.js +110 -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 +52 -10
  12. package/lib/index.js +267 -324
  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/links.d.ts +25 -0
  18. package/lib/links.js +61 -0
  19. package/lib/links.js.map +1 -0
  20. package/lib/preview-token.d.ts +21 -0
  21. package/lib/preview-token.js +37 -0
  22. package/lib/preview-token.js.map +1 -0
  23. package/lib/state.d.ts +17 -0
  24. package/lib/state.js +44 -0
  25. package/lib/state.js.map +1 -0
  26. package/lib/storage.d.ts +6 -0
  27. package/lib/storage.js +24 -0
  28. package/lib/storage.js.map +1 -0
  29. package/lib/tags/blog.d.ts +5 -2
  30. package/lib/tags/blog.js +25 -41
  31. package/lib/tags/blog.js.map +1 -1
  32. package/lib/tags/breadcrumb.d.ts +2 -1
  33. package/lib/tags/breadcrumb.js +13 -15
  34. package/lib/tags/breadcrumb.js.map +1 -1
  35. package/lib/tags/image.d.ts +2 -0
  36. package/lib/tags/image.js +25 -68
  37. package/lib/tags/image.js.map +1 -1
  38. package/lib/tags/index.js +5 -1
  39. package/lib/tags/index.js.map +1 -1
  40. package/lib/tags/module.d.ts +1 -0
  41. package/lib/tags/module.js +6 -6
  42. package/lib/tags/module.js.map +1 -1
  43. package/lib/tags/navigation.d.ts +5 -2
  44. package/lib/tags/navigation.js +14 -217
  45. package/lib/tags/navigation.js.map +1 -1
  46. package/lib/tags/page.d.ts +5 -2
  47. package/lib/tags/page.js +9 -21
  48. package/lib/tags/page.js.map +1 -1
  49. package/lib/tags/region.d.ts +1 -0
  50. package/lib/tags/region.js +6 -7
  51. package/lib/tags/region.js.map +1 -1
  52. package/package.json +25 -26
  53. package/lib/test-tags.d.ts +0 -3
  54. package/lib/test-tags.js +0 -125
  55. package/lib/test-tags.js.map +0 -1
  56. package/lib/test.d.ts +0 -1
  57. package/lib/test.js +0 -52
  58. 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,67 @@ 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.wrapLinks = exports.LinkDrop = 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");
47
+ const links_1 = require("./links");
31
48
  const cheerio = __importStar(require("cheerio"));
32
- const body_parser_1 = __importDefault(require("body-parser"));
33
49
  const tags_1 = require("./tags");
34
50
  const filters_1 = require("./filters");
35
- const aws_sdk_1 = require("aws-sdk");
36
51
  const interfaces_1 = require("./interfaces");
52
+ const storage_1 = require("./storage");
53
+ const state_1 = require("./state");
54
+ const draft_1 = require("./draft");
55
+ __exportStar(require("./interfaces"), exports);
56
+ var preview_token_1 = require("./preview-token");
57
+ Object.defineProperty(exports, "signPreviewToken", { enumerable: true, get: function () { return preview_token_1.signPreviewToken; } });
58
+ Object.defineProperty(exports, "verifyPreviewToken", { enumerable: true, get: function () { return preview_token_1.verifyPreviewToken; } });
59
+ var state_2 = require("./state");
60
+ Object.defineProperty(exports, "renderState", { enumerable: true, get: function () { return state_2.renderState; } });
61
+ var links_2 = require("./links");
62
+ Object.defineProperty(exports, "LinkDrop", { enumerable: true, get: function () { return links_2.LinkDrop; } });
63
+ Object.defineProperty(exports, "wrapLinks", { enumerable: true, get: function () { return links_2.wrapLinks; } });
64
+ /**
65
+ * A Cee.ms site renderer: an Express app that renders published pages, and verified editor drafts, with the
66
+ * site's Liquid templates.
67
+ *
68
+ * One instance serves every request, so nothing about a request is stored on it. Each render carries its own
69
+ * `RenderState` (site, page, edit mode), passed to templates and tags as render-time globals.
70
+ */
37
71
  class Ceems {
72
+ app;
73
+ engine;
74
+ config;
75
+ /** The latest published `site.json`, refreshed on every public request. Never holds draft data. */
76
+ site;
77
+ storage;
78
+ drafts;
38
79
  constructor(config) {
39
80
  this.config = config;
40
81
  // set defaults
@@ -44,367 +85,269 @@ class Ceems {
44
85
  if (!this.config.viewsPath) {
45
86
  this.config.viewsPath = './views/';
46
87
  }
47
- if (!this.config.liquidCache) {
88
+ if (this.config.liquidCache === undefined) {
48
89
  this.config.liquidCache = true;
49
90
  }
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();
91
+ this.config.tags = this.config.tags || [];
92
+ this.config.filters = this.config.filters || [];
93
+ this.storage =
94
+ this.config.cacheSource === interfaces_1.CacheSource.S3
95
+ ? (0, storage_1.s3Storage)(this.config.cacheBucket || '', this.config.cacheRegion || process.env.AWS_REGION)
96
+ : (0, storage_1.localStorage)(this.config.cacheFolder || './');
97
+ // One engine for the life of the process. Site data is passed per render, never through these options.
98
+ this.engine = new liquidjs_1.Liquid({
99
+ root: path_1.default.resolve(this.config.viewsPath),
100
+ extname: '.liquid',
101
+ cache: this.config.liquidCache,
102
+ jsTruthy: true,
103
+ globals: { ...(this.config.globals || {}) },
104
+ });
105
+ this.registerTags();
106
+ this.app = (0, express_1.default)();
57
107
  this.app.set('case sensitive routing', true);
58
- // process config and set defaults
59
- // // set up static file hosting
108
+ this.app.engine('liquid', this.engine.express());
109
+ this.app.set('views', this.config.viewsPath);
110
+ this.app.set('view engine', 'liquid');
60
111
  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
112
+ this.app.use(express_1.default.json({ limit: this.config.parseLimit || '250kb' }));
113
+ this.app.use(express_1.default.urlencoded({ extended: false, limit: this.config.parseLimit || '250kb' }));
114
+ // Drafts are registered before the published-site middleware: they never read or replace `this.site`.
115
+ this.drafts = (0, draft_1.draftServiceFor)(this.config.preview);
116
+ this.app.get('/draft/:pageId', (req, res) => this.handleDraft(req, res));
117
+ // load the published site map for every page request
90
118
  this.app.use((req, res, next) => {
91
119
  this.loadSite().then((site) => {
120
+ // each request renders against the snapshot it started with
121
+ req.ceemsSite = site;
92
122
  next();
93
- });
123
+ }, (error) => next(error));
94
124
  });
95
125
  if (!this.config.customHandler) {
96
126
  this.app.get(/(.*)/, async (req, res) => {
97
- let page = await this.load(req, res, true);
98
- this.render(page, req, res);
127
+ try {
128
+ const page = await this.load(req, res, true);
129
+ await this.render(page, req, res);
130
+ }
131
+ catch (error) {
132
+ if (!(0, state_1.isNotFound)(error))
133
+ console.error('[ceems] load failed', error);
134
+ if (!res.headersSent)
135
+ res.status(404).send('Not found');
136
+ }
99
137
  });
100
138
  }
101
- // init routes
102
- this.app.post(/draft\/(.*)/, async (req, res) => {
103
- await this.postDraft(req, res);
104
- });
105
139
  }
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
- }
140
+ registerTags() {
141
+ (0, tags_1.registerRegionTag)(this.engine);
142
+ (0, tags_1.registerModuleTag)(this.engine);
143
+ (0, tags_1.registerNavigationTag)(this.engine);
144
+ (0, tags_1.registerBlogTag)(this.engine);
145
+ (0, tags_1.registerPageTag)(this.engine, this);
146
+ (0, tags_1.registerImageTag)(this.engine);
147
+ (0, tags_1.registerImageUrlTag)(this.engine);
148
+ (0, tags_1.registerBreadcrumbTag)(this.engine);
149
+ (0, filters_1.registerActiveFilter)(this.engine);
150
+ (0, filters_1.registerSlugifyFilter)(this.engine);
151
+ // custom tags and filters
152
+ for (const tag of this.config.tags || []) {
153
+ this.engine.registerTag(tag.name, tag.register(this));
125
154
  }
126
- else {
127
- sitePromise = new Promise((resolve, reject) => {
128
- resolve(draft);
129
- });
155
+ for (const filter of this.config.filters || []) {
156
+ this.engine.registerFilter(filter.name, filter.filter);
130
157
  }
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
158
  }
159
+ /** Loads the published `site.json`, remembers it as `this.site`, and returns it. */
160
+ async loadSite() {
161
+ const site = JSON.parse(await this.storage.read('pages/site.json'));
162
+ this.site = site;
163
+ return site;
164
+ }
165
+ /**
166
+ * Loads the published page for the request's path (or the `404` page), ready for `render`. With
167
+ * `loadPage` false, returns `page` as given (for custom routes that build their own page object).
168
+ */
142
169
  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');
170
+ const site = req.ceemsSite || this.site || (await this.loadSite());
148
171
  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;
172
+ const { path, pagination } = this.parsePath(req);
173
+ const entry = site.pages[path];
174
+ if (entry) {
175
+ page = await this.loadPageFile(entry._id);
176
+ page.fullpath = entry.url == 'home' ? '' : entry.url;
160
177
  page.path = page.fullpath.split('/');
161
- if (this.pagination)
162
- page.pagination = this.pagination;
178
+ if (pagination)
179
+ page.pagination = pagination;
163
180
  }
164
- // 404
165
181
  else {
166
- console.log('404 for ', path);
167
182
  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');
183
+ const notFound = site.pages['404'];
184
+ if (notFound) {
185
+ page = await this.loadPageFile(notFound._id);
175
186
  }
176
187
  }
177
188
  }
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;
189
+ if (!page) {
190
+ throw new state_1.NotFoundError('No page');
191
+ }
192
+ return (0, state_1.attachState)(page, { site, page, edit: false });
196
193
  }
194
+ /** Reads `pages/<id>.json`. Stored links read as their address in templates (see `LinkDrop`). */
195
+ async loadPageFile(id) {
196
+ return (0, links_1.wrapLinks)(JSON.parse(await this.storage.read('pages/' + id + '.json')));
197
+ }
198
+ /** Synchronous local read, kept for custom tags. `filename` is a full path. */
197
199
  loadLocalFile(filename) {
198
- return JSON.parse(fs_1.default.readFileSync(filename, 'utf8'));
200
+ return (0, links_1.wrapLinks)(JSON.parse(fs_1.default.readFileSync(filename, 'utf8')));
199
201
  }
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
- });
202
+ /** Reads and parses a published JSON file, kept for custom tags. `filename` is relative to the bucket. */
203
+ async loadS3File(filename) {
204
+ return (0, links_1.wrapLinks)(JSON.parse(await this.storage.read(filename)));
210
205
  }
211
- getPath(req) {
206
+ /** The site path for a request (`home` for `/`), and the blog page number if the URL ends in one. */
207
+ parsePath(req) {
212
208
  if (req.overriddenPath) {
213
- return req.overriddenPath;
209
+ return { path: req.overriddenPath, pagination: null };
214
210
  }
215
211
  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)) {
212
+ let path = (req._parsedUrl?.pathname || req.path).substring(1);
213
+ let pagination = null;
214
+ const matches = path.match(regex);
215
+ if (matches) {
220
216
  // looks like a blog url
221
- const matches = path.match(regex);
222
217
  path = matches[1];
223
- this.pagination = parseInt(matches[2].substring(1));
218
+ pagination = parseInt(matches[2].substring(1));
224
219
  }
225
220
  if (path === '')
226
221
  path = 'home';
227
- return path;
222
+ return { path, pagination };
223
+ }
224
+ /** @deprecated Use `parsePath`. */
225
+ getPath(req) {
226
+ return this.parsePath(req).path;
227
+ }
228
+ /** The Liquid globals for one render: the site data, static globals, and the render state. */
229
+ globalsFor(state) {
230
+ return { ...state.site, ...this.engine.options.globals, [state_1.RENDER_STATE]: state };
231
+ }
232
+ async renderModule(module, state) {
233
+ const scope = {
234
+ ...module.content,
235
+ attributes: { _id: module._id, region: module.region, order: module.order },
236
+ page: state.page,
237
+ site: state.site.site,
238
+ pages: state.site.pages,
239
+ navigation: state.site.navigation,
240
+ };
241
+ let html = await this.engine.renderFile(module.type, scope, { globals: this.globalsFor(state) });
242
+ if (state.edit) {
243
+ const $ = cheerio.load(html, null, false);
244
+ $.root().children().attr('data-module-type', module.type);
245
+ if (module._id) {
246
+ $.root().children().attr('data-module-id', String(module._id));
247
+ }
248
+ html = $.html();
249
+ }
250
+ return html;
251
+ }
252
+ async renderRegion(key, modules, state) {
253
+ const html = (await Promise.all(modules.map((module) => this.renderModule(module, state)))).join('');
254
+ // In drafts, comments mark each region so the editor can replace one without reloading the page.
255
+ return state.edit ? `<!--ceems:region:${key}-->${html}<!--/ceems:region:${key}-->` : html;
228
256
  }
229
- render(page, req, res, send = true) {
257
+ /** Renders a full page with its layout template. */
258
+ async renderPage(state) {
259
+ const page = state.page;
230
260
  if (this.config.whitelistedCookies) {
231
261
  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
262
  }
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
- });
263
+ const regions = {};
264
+ await Promise.all(Object.entries(page.regions || {}).map(async ([key, region]) => {
265
+ regions[key] = await this.renderRegion(key, region.modules || [], state);
266
+ }));
267
+ // modules not in a region, output with `{% module name %}`
268
+ const modules = {};
269
+ await Promise.all(Object.entries(page.modules || {}).map(async ([key, module]) => {
270
+ modules[key] = typeof module === 'string' ? module : await this.renderModule(module, state);
271
+ // legacy templates may read the rendered HTML from the page itself
272
+ page.modules[key] = modules[key];
273
+ }));
274
+ const scope = { regions, modules, navigation: state.site.navigation || {}, page };
275
+ let html = await this.engine.renderFile(page.template, scope, { globals: this.globalsFor(state) });
276
+ if (state.edit && this.drafts) {
277
+ html = this.drafts.injectBridge(html);
278
+ }
279
+ return html;
371
280
  }
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));
281
+ /**
282
+ * Renders a page from `load`. Sends the HTML, or returns it when `send` is false. On failure, sends a
283
+ * generic error (404 for a blog page past the last article) and resolves to undefined.
284
+ */
285
+ async render(page, req, res, send = true) {
286
+ const state = (0, state_1.stateOf)(page) || { site: this.site || (await this.loadSite()), page, edit: false };
287
+ try {
288
+ const html = await this.renderPage(state);
289
+ if (!send)
290
+ return html;
291
+ res.send(html);
378
292
  }
379
- else {
380
- page = req.body.page;
293
+ catch (error) {
294
+ const status = (0, state_1.isNotFound)(error) ? 404 : 500;
295
+ if (status === 500)
296
+ console.error('[ceems] render failed', error);
297
+ if (!res.headersSent)
298
+ res.status(status).send(status === 404 ? 'Not found' : 'Something went wrong');
381
299
  }
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));
300
+ }
301
+ /**
302
+ * `GET /draft/:pageId?token=…[&module=<id>|&region=<key>]`: a verified editor draft, as a full page, one
303
+ * module, or one region. The token is checked here, then used to fetch the draft from the Cee.ms API.
304
+ */
305
+ async handleDraft(req, res) {
306
+ if (!this.drafts) {
307
+ res.status(404).send('Not found');
308
+ return;
385
309
  }
386
- else {
387
- this.site = req.body.site;
310
+ const draft = await this.drafts.fetch(req.params.pageId, String(req.query.token || ''));
311
+ if (!draft.ok) {
312
+ this.drafts.setHeaders(res);
313
+ res.status(draft.status).send(draft.status === 401 ? 'Unauthorized' : 'Not available');
314
+ return;
388
315
  }
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;
316
+ const { page, site } = draft;
317
+ // find the page's full path from the site map
318
+ const entry = Object.values(site.pages || {}).find((p) => p._id == page._id);
319
+ page.fullpath = entry ? (entry.url == 'home' ? '' : entry.url) : page.handle;
320
+ page.path = page.fullpath.split('/');
321
+ const state = { site, page, edit: true };
322
+ try {
323
+ let html;
324
+ if (typeof req.query.module === 'string') {
325
+ const module = Object.values(page.regions || {})
326
+ .flatMap((region) => region.modules || [])
327
+ .find((m) => String(m._id) === req.query.module);
328
+ if (!module)
329
+ throw new state_1.NotFoundError('No such module');
330
+ html = await this.renderModule(module, state);
394
331
  }
395
- });
396
- if (path && this.site.pages[path]) {
397
- page.fullpath = this.site.pages[path].url == 'home' ? '' : this.site.pages[path].url;
332
+ else if (typeof req.query.region === 'string') {
333
+ const region = page.regions?.[req.query.region];
334
+ if (!region)
335
+ throw new state_1.NotFoundError('No such region');
336
+ html = await this.renderRegion(req.query.region, region.modules || [], state);
337
+ }
338
+ else {
339
+ html = await this.renderPage(state);
340
+ }
341
+ this.drafts.setHeaders(res);
342
+ res.type('html').send(html);
398
343
  }
399
- else {
400
- page.fullpath = page.handle;
344
+ catch (error) {
345
+ const status = (0, state_1.isNotFound)(error) ? 404 : 500;
346
+ if (status === 500)
347
+ console.error('[ceems] draft render failed', error);
348
+ this.drafts.setHeaders(res);
349
+ res.status(status).send(status === 404 ? 'Not found' : 'Something went wrong');
401
350
  }
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
351
  }
409
352
  }
410
353
  exports.Ceems = Ceems;