@nakedcreativity/cee-ms-node 1.0.122 → 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.
- package/README.md +81 -0
- package/lib/draft.d.ts +20 -0
- package/lib/draft.js +109 -0
- package/lib/draft.js.map +1 -0
- package/lib/filters/active.js +1 -2
- package/lib/filters/active.js.map +1 -1
- package/lib/filters/index.js +5 -1
- package/lib/filters/index.js.map +1 -1
- package/lib/filters/slugify.js +3 -4
- package/lib/filters/slugify.js.map +1 -1
- package/lib/index.d.ts +51 -10
- package/lib/index.js +262 -323
- package/lib/index.js.map +1 -1
- package/lib/interfaces.d.ts +55 -16
- package/lib/interfaces.js +1 -1
- package/lib/interfaces.js.map +1 -1
- package/lib/preview-token.d.ts +21 -0
- package/lib/preview-token.js +37 -0
- package/lib/preview-token.js.map +1 -0
- package/lib/state.d.ts +17 -0
- package/lib/state.js +44 -0
- package/lib/state.js.map +1 -0
- package/lib/storage.d.ts +6 -0
- package/lib/storage.js +24 -0
- package/lib/storage.js.map +1 -0
- package/lib/tags/blog.d.ts +5 -2
- package/lib/tags/blog.js +25 -41
- package/lib/tags/blog.js.map +1 -1
- package/lib/tags/breadcrumb.d.ts +2 -1
- package/lib/tags/breadcrumb.js +13 -15
- package/lib/tags/breadcrumb.js.map +1 -1
- package/lib/tags/image.d.ts +2 -0
- package/lib/tags/image.js +25 -68
- package/lib/tags/image.js.map +1 -1
- package/lib/tags/index.js +5 -1
- package/lib/tags/index.js.map +1 -1
- package/lib/tags/module.d.ts +1 -0
- package/lib/tags/module.js +6 -6
- package/lib/tags/module.js.map +1 -1
- package/lib/tags/navigation.d.ts +5 -2
- package/lib/tags/navigation.js +14 -217
- package/lib/tags/navigation.js.map +1 -1
- package/lib/tags/page.d.ts +5 -2
- package/lib/tags/page.js +9 -21
- package/lib/tags/page.js.map +1 -1
- package/lib/tags/region.d.ts +1 -0
- package/lib/tags/region.js +6 -7
- package/lib/tags/region.js.map +1 -1
- package/package.json +25 -26
- package/lib/test-tags.d.ts +0 -3
- package/lib/test-tags.js +0 -125
- package/lib/test-tags.js.map +0 -1
- package/lib/test.d.ts +0 -1
- package/lib/test.js +0 -52
- 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.
|
|
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 (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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 (
|
|
84
|
+
if (this.config.liquidCache === undefined) {
|
|
48
85
|
this.config.liquidCache = true;
|
|
49
86
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
this.
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
page = this.
|
|
153
|
-
|
|
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 (
|
|
162
|
-
page.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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
380
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* `GET /draft/:pageId?token=…[&module=<id>|®ion=<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
|
-
|
|
387
|
-
|
|
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
|
-
|
|
390
|
-
// find
|
|
391
|
-
Object.
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
397
|
-
|
|
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
|
-
|
|
400
|
-
|
|
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;
|