Merge branch 'v1.2-dev' as v1.2.2
[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 return `<tr>
65 <th scope="row">${detailsLink ? '<a href="topic/' + topic.id + '">' : ''}${topic.url}${detailsLink ? '</a>' : ''}</th>
66 <td>${subscribers.length}</td>
67 <td>${dateOrNot(topic.created, 'Unknown')}</td>
68 <td>${secondsToPeriod(topic.leaseSecondsPreferred)}</td>
69 <td>${secondsToPeriod(topic.leaseSecondsMin)}</td>
70 <td>${secondsToPeriod(topic.leaseSecondsMax)}</td>
71 <td>${topic.publisherValidationUrl ? topic.publisherValidationUrl : 'None'}</td>
72 <td>${topic.isActive}</td>
73 <td>${topic.isDeleted}</td>
74 <td>${dateOrNot(topic.lastPublish, 'Never')}</td>
75 <td>${dateOrNot(topic.contentFetchNextAttempt, 'Next Publish')}</td>
76 <td>${topic.contentFetchAttemptsSinceSuccess}</td>
77 <td>${dateOrNot(topic.contentUpdated, 'Never')}</td>
78 <td>${topic.contentType}</td>
79 <td>${topic.id}</td>
80 </tr>`;
81 }
82
83
84 /**
85 * Render the header row for topic details.
86 * @returns {String}
87 */
88 function renderTopicRowHeader() {
89 return `<tr>
90 <th scope="col">Topic URL</th>
91 <th scope="col">Subscribers</th>
92 <th scope="col">Created</th>
93 <th scope="col">Lease Time Preferred</th>
94 <th scope="col">Lease Time Minimum</th>
95 <th scope="col">Lease Time Maximum</th>
96 <th scope="col">Publisher Validation URL</th>
97 <th scope="col">Active</th>
98 <th scope="col">Deleted</th>
99 <th scope="col">Last Publish Notification</th>
100 <th scope="col">Next Content Fetch</th>
101 <th scope="col">Content Fetch Failures</th>
102 <th scope="col">Content Updated</th>
103 <th scope="col">Content Type</th>
104 <th scope="col">ID</th>
105 </tr>`;
106 }
107
108
109 /**
110 * Render a subscription as a row of details.
111 * @param {Object} subscription
112 * @returns {String}
113 */
114 function renderSubscriptionRow(subscription) {
115 return `<tr>
116 <td scope="row">${subscription.callback}</td>
117 <td>${dateOrNot(subscription.created, 'Unknown')}</td>
118 <td>${dateOrNot(subscription.verified, 'Never')}</td>
119 <td>${dateOrNot(subscription.expires, 'Never')}</td>
120 <td>${!!subscription.secret}</td>
121 <td>${subscription.signatureAlgorithm}</td>
122 <td>${subscription.httpRemoteAddr}</td>
123 <td>${subscription.httpFrom}</td>
124 <td>${dateOrNot(subscription.contentDelivered, 'Never')}</td>
125 <td>${subscription.deliveryAttemptsSinceSuccess}</td>
126 <td>${dateOrNot(subscription.deliveryNextAttempt, 'Next Publish')}</td>
127 <td>${subscription.id}</td>
128 </tr>`;
129 }
130
131
132 /**
133 * Render a row of headers for subscription details.
134 * @returns {String}
135 */
136 function renderSubscriptionRowHeader() {
137 return `<tr>
138 <th scope="col">Callback URL</th>
139 <th scope="col">Created</th>
140 <th scope="col">Verified</th>
141 <th scope="col">Expires</th>
142 <th scope="col">Using Secret</th>
143 <th scope="col">Signature Type</th>
144 <th scope="col">Remote Address</th>
145 <th scope="col">From</th>
146 <th scope="col">Content Delivered</th>
147 <th scope="col">Content Delivery Failures</th>
148 <th scope="col">Next Delivery</th>
149 <th scope="col">ID</th>
150 </tr>
151 `;
152 }
153
154
155 /**
156 * Render the preamble for an HTML page, up through body.
157 * @param {Number} pagePathLevel number of paths below root this page is
158 * @param {String} pageTitle
159 * @param {String[]} headElements
160 * @returns
161 */
162 function htmlHead(pagePathLevel, pageTitle, headElements = []) {
163 const rootPathPfx = '../'.repeat(pagePathLevel);
164 return `<!DOCTYPE html>
165 <html lang="en>
166 <head>
167 <meta charset="utf-8">` +
168 headElements.map((e) => `${' '.repeat(2)}${e}`).join('\n') + `
169 <title>${pageTitle}</title>
170 <link rel="stylesheet" href="${rootPathPfx}static/theme.css">
171 </head>
172 <body>`;
173 }
174
175
176 /**
177 * Closes remainder of HTML page body.
178 * @returns {String}
179 */
180 function htmlTail() {
181 return ` </body>
182 </html>`;
183 }
184
185
186 /**
187 * Render a navigation link for the header section.
188 * @param {Object} nav
189 * @param {String} nav.href
190 * @param {String} nav.class
191 * @param {String} nav.text
192 * @returns {String}
193 */
194 function renderNavLink(nav) {
195 return `<li>
196 <a href="${nav.href}"${nav.class ? (' class="' + nav.class + '"') : ''}>${nav.text}</a>
197 </li>`;
198 }
199
200
201 /**
202 * Render the navigation header, and open the main section.
203 * @param {String} pageTitle
204 * @param {Object[]} navLinks
205 * @returns {String}
206 */
207 function htmlHeader(pageTitle, navLinks = []) {
208 return ` <header>
209 <h1>${pageTitle}</h1>
210 <nav>` +
211 (navLinks.length ? `
212 <ol>
213 ${navLinks.map((l) => renderNavLink(l)).join('\n')}
214 </ol>`
215 : '') + `
216 </nav>
217 </header>
218 <main>`;
219 }
220
221
222 /**
223 * Close the main section and finish off with boilerplate.
224 * @param {String[]} footerEntries
225 * @returns {String}
226 */
227 function htmlFooter(footerEntries = []) {
228 return ` </main>
229 <footer>` +
230 (footerEntries.length ? `
231 <ol>` + footerEntries.map((f) => ` <li>${f}</li>`).join('\n') + `
232 </ol>`
233 : '') + `
234 </footer>`;
235 }
236
237
238 /**
239 * Render all parts of an HTML page.
240 * @param {Number} pagePathLevel
241 * @param {String} pageTitle
242 * @param {String[]} headElements
243 * @param {Object[]} navLinks
244 * @param {String[]} main
245 * @param {String[]} footerEntries
246 * @returns {String}
247 */
248 function htmlTemplate(pagePathLevel, pageTitle, headElements = [], navLinks = [], main = [], footerEntries = []) {
249 return [
250 htmlHead(pagePathLevel, pageTitle, headElements),
251 htmlHeader(pageTitle, navLinks),
252 ...main,
253 htmlFooter(footerEntries),
254 htmlTail(),
255 ].join('\n');
256 }
257
258
259 module.exports = {
260 dateOrNot,
261 secondsToPeriod,
262 htmlHeader,
263 htmlFooter,
264 htmlHead,
265 htmlTail,
266 renderNavLink,
267 renderTopicRowHeader,
268 renderTopicRow,
269 renderSubscriptionRowHeader,
270 renderSubscriptionRow,
271 htmlTemplate,
272 };