fix html template
[websub-hub] / src / template / template-helper.js
1 'use strict';
2
3 /**
4 * A bunch of shorthand to put together common parts of an HTML page.
5 */
6
7 /**
8 * Some fields may have values outside normal dates, handle them here.
9 * @param {Date} date
10 * @param {String} otherwise
11 */
12 const dateOrNot = (date, otherwise) => {
13 if (!date) {
14 return otherwise;
15 }
16 if (typeof date === 'number') {
17 date = new Date(date);
18 }
19 const dateMs = date.getTime();
20 if (!Number.isFinite(dateMs)
21 || dateMs == 0) {
22 return otherwise;
23 }
24 return date.toString();
25 };
26
27
28 /**
29 * Render a duration.
30 * @param {Number} seconds
31 * @returns {String}
32 */
33 const secondsToPeriod = (seconds) => {
34 let value = seconds;
35 const result = [];
36
37 const nextResult = (factor, label) => {
38 const r = factor ? value % factor : value;
39 if (r) {
40 result.push(`${r} ${label}${r != 1 ? 's' : ''}`);
41 }
42 value = factor ? Math.floor(value / factor) : value;
43 }
44
45 nextResult(60, 'second');
46 nextResult(60, 'minute');
47 nextResult(24, 'hour');
48 nextResult(30, 'day');
49 nextResult(undefined, 'month');
50
51 result.reverse();
52 return result.join(' ');
53 };
54
55
56 /**
57 * Render a topic as a row of details.
58 * @param {Object} topic
59 * @param {Object[]} subscribers
60 * @param {Boolean} detailsLink
61 * @returns {String}
62 */
63 function renderTopicRow(topic, subscribers, detailsLink = true) {
64 if (!topic) {
65 return `<tr>
66 <th colspan="15">(topic not found)</th>
67 </tr>`;
68 }
69 return `<tr>
70 <th scope="row">${detailsLink ? '<a href="topic/' + topic.id + '">' : ''}${topic.url}${detailsLink ? '</a>' : ''}</th>
71 <td>${subscribers.length}</td>
72 <td>${dateOrNot(topic.created, 'Unknown')}</td>
73 <td>${secondsToPeriod(topic.leaseSecondsPreferred)}</td>
74 <td>${secondsToPeriod(topic.leaseSecondsMin)}</td>
75 <td>${secondsToPeriod(topic.leaseSecondsMax)}</td>
76 <td>${topic.publisherValidationUrl ? topic.publisherValidationUrl : 'None'}</td>
77 <td>${topic.isActive}</td>
78 <td>${topic.isDeleted}</td>
79 <td>${dateOrNot(topic.lastPublish, 'Never')}</td>
80 <td>${dateOrNot(topic.contentFetchNextAttempt, 'Next Publish')}</td>
81 <td>${topic.contentFetchAttemptsSinceSuccess}</td>
82 <td>${dateOrNot(topic.contentUpdated, 'Never')}</td>
83 <td>${topic.contentType}</td>
84 <td>${topic.id}</td>
85 </tr>`;
86 }
87
88
89 /**
90 * Render the header row for topic details.
91 * @returns {String}
92 */
93 function renderTopicRowHeader() {
94 return `<tr>
95 <th scope="col">Topic URL</th>
96 <th scope="col">Subscribers</th>
97 <th scope="col">Created</th>
98 <th scope="col">Lease Time Preferred</th>
99 <th scope="col">Lease Time Minimum</th>
100 <th scope="col">Lease Time Maximum</th>
101 <th scope="col">Publisher Validation URL</th>
102 <th scope="col">Active</th>
103 <th scope="col">Deleted</th>
104 <th scope="col">Last Publish Notification</th>
105 <th scope="col">Next Content Fetch</th>
106 <th scope="col">Content Fetch Failures</th>
107 <th scope="col">Content Updated</th>
108 <th scope="col">Content Type</th>
109 <th scope="col">ID</th>
110 </tr>`;
111 }
112
113
114 /**
115 * Render a subscription as a row of details.
116 * @param {Object} subscription
117 * @returns {String}
118 */
119 function renderSubscriptionRow(subscription) {
120 if (!subscription) {
121 return `<tr>
122 <th colspan="12">(topic not found)</th>
123 </tr>`;
124 }
125 return `<tr>
126 <td scope="row">${subscription.callback}</td>
127 <td>${dateOrNot(subscription.created, 'Unknown')}</td>
128 <td>${dateOrNot(subscription.verified, 'Never')}</td>
129 <td>${dateOrNot(subscription.expires, 'Never')}</td>
130 <td>${!!subscription.secret}</td>
131 <td>${subscription.signatureAlgorithm}</td>
132 <td>${subscription.httpRemoteAddr}</td>
133 <td>${subscription.httpFrom}</td>
134 <td>${dateOrNot(subscription.contentDelivered, 'Never')}</td>
135 <td>${subscription.deliveryAttemptsSinceSuccess}</td>
136 <td>${dateOrNot(subscription.deliveryNextAttempt, 'Next Publish')}</td>
137 <td>${subscription.id}</td>
138 </tr>`;
139 }
140
141
142 /**
143 * Render a row of headers for subscription details.
144 * @returns {String}
145 */
146 function renderSubscriptionRowHeader() {
147 return `<tr>
148 <th scope="col">Callback URL</th>
149 <th scope="col">Created</th>
150 <th scope="col">Verified</th>
151 <th scope="col">Expires</th>
152 <th scope="col">Using Secret</th>
153 <th scope="col">Signature Type</th>
154 <th scope="col">Remote Address</th>
155 <th scope="col">From</th>
156 <th scope="col">Content Delivered</th>
157 <th scope="col">Content Delivery Failures</th>
158 <th scope="col">Next Delivery</th>
159 <th scope="col">ID</th>
160 </tr>
161 `;
162 }
163
164
165 /**
166 * Render the preamble for an HTML page, up through body.
167 * @param {Number} pagePathLevel number of paths below root this page is
168 * @param {String} pageTitle
169 * @param {String[]} headElements
170 * @returns
171 */
172 function htmlHead(pagePathLevel, pageTitle, headElements = []) {
173 const rootPathPfx = '../'.repeat(pagePathLevel);
174 return `<!DOCTYPE html>
175 <html lang="en">
176 <head>
177 <meta charset="utf-8">` +
178 headElements.map((e) => `${' '.repeat(2)}${e}`).join('\n') + `
179 <title>${pageTitle}</title>
180 <link rel="stylesheet" href="${rootPathPfx}static/theme.css">
181 </head>
182 <body>`;
183 }
184
185
186 /**
187 * Closes remainder of HTML page body.
188 * @returns {String}
189 */
190 function htmlTail() {
191 return ` </body>
192 </html>`;
193 }
194
195
196 /**
197 * Render a navigation link for the header section.
198 * @param {Object} nav
199 * @param {String} nav.href
200 * @param {String} nav.class
201 * @param {String} nav.text
202 * @returns {String}
203 */
204 function renderNavLink(nav) {
205 return `<li>
206 <a href="${nav.href}"${nav.class ? (' class="' + nav.class + '"') : ''}>${nav.text}</a>
207 </li>`;
208 }
209
210
211 /**
212 * Render the navigation header, and open the main section.
213 * @param {String} pageTitle
214 * @param {Object[]} navLinks
215 * @returns {String}
216 */
217 function htmlHeader(pageTitle, navLinks = []) {
218 return ` <header>
219 <h1>${pageTitle}</h1>
220 <nav>` +
221 (navLinks.length ? `
222 <ol>
223 ${navLinks.map((l) => renderNavLink(l)).join('\n')}
224 </ol>`
225 : '') + `
226 </nav>
227 </header>
228 <main>`;
229 }
230
231
232 /**
233 * Close the main section and finish off with boilerplate.
234 * @param {String[]} footerEntries
235 * @returns {String}
236 */
237 function htmlFooter(footerEntries = []) {
238 return ` </main>
239 <footer>` +
240 (footerEntries.length ? `
241 <ol>` + footerEntries.map((f) => ` <li>${f}</li>`).join('\n') + `
242 </ol>`
243 : '') + `
244 </footer>`;
245 }
246
247
248 /**
249 * Render all parts of an HTML page. Adds user logout nav link automatically.
250 * @param {Object} ctx
251 * @param {Number} pagePathLevel
252 * @param {String} pageTitle
253 * @param {String[]} headElements
254 * @param {Object[]} navLinks
255 * @param {String[]} main
256 * @param {String[]} footerEntries
257 * @returns {String}
258 */
259 function htmlTemplate(ctx, pagePathLevel, pageTitle, headElements = [], navLinks = [], main = [], footerEntries = []) {
260 const user = (ctx && ctx.session && ctx.session.authenticatedProfile) || (ctx && ctx.session && ctx.session.authenticatedIdentifier);
261 if (user) {
262 let logoutPath;
263 if (pagePathLevel > 0) {
264 logoutPath = `${'../'.repeat(pagePathLevel - 1)}`;
265 } else {
266 logoutPath = 'admin/';
267 }
268 navLinks.push({
269 text: `Logout (${user})`,
270 href: `${logoutPath}logout`,
271 });
272 }
273 return [
274 htmlHead(pagePathLevel, pageTitle, headElements),
275 htmlHeader(pageTitle, navLinks),
276 ...main,
277 htmlFooter(footerEntries),
278 htmlTail(),
279 ].join('\n');
280 }
281
282
283 module.exports = {
284 dateOrNot,
285 secondsToPeriod,
286 htmlHeader,
287 htmlFooter,
288 htmlHead,
289 htmlTail,
290 renderNavLink,
291 renderTopicRowHeader,
292 renderTopicRow,
293 renderSubscriptionRowHeader,
294 renderSubscriptionRow,
295 htmlTemplate,
296 };