logoUrl now resolves the proper path depth, for path depths > 1
[squeep-html-template-helper] / lib / 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 the preamble <head> for an HTML page.
58 * @param {Number} pagePathLevel number of paths below root this page is
59 * @param {Object} ctx
60 * @param {Object} options
61 * @param {String[]=} options.headElements
62 * @param {String=} options.pageTitle
63 * @returns {String}
64 */
65 function htmlHead(pagePathLevel, ctx, options) {
66 const rootPathPfx = '../'.repeat(pagePathLevel);
67 const {
68 headElements = [],
69 pageTitle = '',
70 } = options;
71 return `\t<head>
72 \t\t<meta charset="utf-8">
73 \t\t<link rel="stylesheet" href="${rootPathPfx}static/theme.css">
74 \t\t<link rel="stylesheet" href="${rootPathPfx}static/custom.css">
75 ${headElements.map((e) => '\t\t' + e).join('\n')}
76 \t\t<title>${pageTitle}</title>
77 \t</head>`;
78 }
79
80
81 /**
82 * Render the main content of an HTML page.
83 * @param {Number} pagePathLevel
84 * @param {Object} ctx
85 * @param {Object} options
86 * @param {Object} options.bodyAttributes
87 * @param {String[]} main
88 * @returns {String}
89 */
90 function htmlBody(pagePathLevel, ctx, options, main = []) {
91 const {
92 bodyAttributes = {},
93 } = options;
94 return `
95 \t<body${elementAttributes(bodyAttributes)}>
96 ${htmlHeader(pagePathLevel, ctx, options)}
97 ${htmlMessages(ctx, options)}
98 \t\t<main>
99 ${main.join('\n')}
100 \t\t</main>
101 ${htmlFooter(ctx, options)}
102 \t</body>`;
103 }
104
105
106 /**
107 * Render a navigation link for the header section.
108 * @param {Object} nav
109 * @param {String} nav.href
110 * @param {String} nav.class
111 * @param {String} nav.text
112 * @returns {String}
113 */
114 function renderNavLink(nav) {
115 return `<a href="${nav.href}"${nav.class ? (' class="' + nav.class + '"') : ''}>${nav.text}</a>`;
116 }
117
118
119 /**
120 * Render the navigation section of the page header.
121 * @param {Object} ctx
122 * @param {Object} options
123 * @param {Object[]=} options.navLinks
124 * @returns {String}
125 */
126 function htmlNav(ctx, options) {
127 const indent = 3;
128 const spacer = '\t'.repeat(indent);
129 const {
130 navLinks = [],
131 } = options;
132 return navLinks.length ? `${spacer}<nav>
133 ${OL(navLinks.map((link) => renderNavLink(link)), indent + 1)}
134 ${spacer}</nav>` : '';
135 }
136
137
138 /**
139 * Render the banner and navigation header.
140 * @param {Number} pagePathLevel
141 * @param {Object} ctx
142 * @param {Object} options
143 * @param {String[]=} options.logoUrl
144 * @param {String[]=} options.pageTitle
145 * @returns {String}
146 */
147 function htmlHeader(pagePathLevel, ctx, options) {
148 const rootPathPfx = '../'.repeat(pagePathLevel);
149 const {
150 logoUrl = '',
151 pageTitle = '',
152 } = options;
153 const logoImg = logoUrl ? `<img src="${rootPathPfx}${logoUrl}" class="logo">` : '';
154 return `\t\t<header>
155 \t\t\t<h1>${logoImg}${pageTitle}</h1>
156 ${htmlNav(ctx, options)}
157 \t\t</header>`;
158 }
159
160
161 /**
162 * Render the bottom boilerplate.
163 * @param {Object} ctx
164 * @param {Object} options
165 * @param {String[]} options.footerEntries
166 * @returns {String}
167 */
168 function htmlFooter(ctx, options) {
169 const indent = 2;
170 const spacer = '\t'.repeat(indent);
171 const {
172 footerEntries = [],
173 } = options;
174
175 return footerEntries.length ? `${spacer}<footer>
176 ${OL(footerEntries, indent + 1)}
177 ${spacer}</footer>` : '';
178 }
179
180
181 /**
182 * Convert an object into element attributes.
183 * @param {Object} attributes
184 * @returns {String}
185 */
186 function elementAttributes(attributes) {
187 const attr = Object.entries(attributes).map(([name, value]) => `${name}="${value}"`).join(' ');
188 return attr ? ' ' + attr : '';
189 }
190
191
192 /**
193 * Wrap an item in a list item element.
194 * @param {String} item
195 * @param {Number} indent
196 * @param {Object} attributes
197 * @returns {String}
198 */
199 function LI(item, indent = 0, attributes = {}) {
200 const spacer = '\t'.repeat(indent);
201 return `${spacer}<li${elementAttributes(attributes)}>${item}</li>`;
202 }
203
204
205 /**
206 * Wrap a list of items in an unordered list.
207 * @param {String[]} items
208 * @param {Number} indent
209 * @param {Object} attributes
210 * @param {(item) => Object} itemAttributeGenerator
211 * @returns {String}
212 */
213 function UL(items, indent = 0, attributes = {}, itemAttributeGenerator = () => {}) {
214 const spacer = '\t'.repeat(indent);
215 return `${spacer}<ul${elementAttributes(attributes)}>
216 ${items.map((item) => LI(item, indent + 1, itemAttributeGenerator(item))).join('\n')}
217 ${spacer}</ul>`;
218 }
219
220
221 /**
222 * Wrap a list of items in an ordered list.
223 * @param {String[]} items
224 * @param {Number} indent
225 * @param {Object} attributes
226 * @param {(item) => Object} itemAttributeGenerator
227 * @returns {String}
228 */
229
230 function OL(items, indent = 0, attributes = {}, itemAttributeGenerator = () => {}) {
231 const spacer = '\t'.repeat(indent);
232 return `${spacer}<ol${elementAttributes(attributes)}>
233 ${items.map((item) => LI(item, indent + 1, itemAttributeGenerator(item))).join('\n')}
234 ${spacer}</ol>`;
235 }
236
237
238 /**
239 * Show any error or notice messages from context.
240 * @param {Object} ctx
241 * @param {String[]=} ctx.errors
242 * @param {String[]=} ctx.notifications
243 * @param {Object} options
244 * @param {String=} options.errorHeading
245 * @param {String[]=} options.errorContent
246 * @param {String=} options.notificationHeading
247 * @param {String[]=} options.notificationContent
248 */
249 function htmlMessages(ctx, options) {
250 const errorHeading = options && options.errorHeading ? `
251 \t<h2>${options.errorHeading}</h2>` : '';
252 const errorContent = options && options.errorContent && options.errorContent.length ? '\n' + options.errorContent.map(((content) => `\t${content}`)).join('\n') : '';
253 const notificationHeading = options && options.notificationHeading ? `\n\t<h2>${options.notificationHeading}</h2>` : '';
254 const notificationContent = options && options.notificationContent && options.notificationContent.length ? '\n' + options.notificationContent.map(((content) => `\t${content}`)).join('\n') : '';
255 const errors = ctx && ctx.errors && ctx.errors.length ? `
256 <section class="error">${errorHeading}${errorContent}
257 ${UL(ctx.errors, 1)}
258 </section>` : '';
259 const notifications = ctx && ctx.notifications && ctx.notifications.length ? `
260 <section class="notification">${notificationHeading}${notificationContent}
261 ${UL(ctx.notifications, 1)}
262 </section>` : '';
263 return [errors, notifications].join('\n');
264 }
265
266
267 /**
268 * Render all parts of an HTML page. Adds user logout nav link automatically.
269 * @param {Number} pagePathLevel - relative path-distance to base
270 * @param {Object} ctx
271 * @param {Object=} ctx.session
272 * @param {String=} ctx.session.authenticatedIdentifier
273 * @param {String=} ctx.session.authenticatedProfile
274 * @param {String[]=} ctx.errors
275 * @param {String[]=} ctx.notifications
276 * @param {Object} options
277 * @param {String=} options.pageTitle
278 * @param {String=} options.logoUrl
279 * @param {Object[]=} options.bodyAttributes
280 * @param {String[]=} options.headElements
281 * @param {Object[]=} options.navLinks
282 * @param {String[]=} options.footerEntries
283 * @param {String=} options.errorHeading
284 * @param {String[]=} options.errorContent
285 * @param {String=} options.notificationHeading
286 * @param {String[]=} options.notificationContent
287 * @param {String[]} main - contents
288 * @returns {String}
289 */
290 function htmlPage(pagePathLevel, ctx, options, main = []) {
291 const user = (ctx && ctx.session && ctx.session.authenticatedProfile) || (ctx && ctx.session && ctx.session.authenticatedIdentifier);
292 if (user) {
293 if (!options.navLinks) {
294 options.navLinks = [];
295 }
296 const logoutRedirect = ctx && ctx.url ? `?r=${encodeURIComponent(ctx.url)}` : '';
297 const logoutPath = (pagePathLevel > 0) ? `${'../'.repeat(pagePathLevel - 1)}` : 'admin/';
298 options.navLinks.push({
299 text: `Logout (${user})`,
300 href: `${logoutPath}logout${logoutRedirect}`,
301 });
302 }
303
304 return [
305 '<!DOCTYPE html>',
306 '<html lang="en">',
307 htmlHead(pagePathLevel, ctx, options),
308 htmlBody(pagePathLevel, ctx, options, main),
309 '</html>',
310 ].join('\n');
311 }
312
313
314 module.exports = {
315 dateOrNot,
316 secondsToPeriod,
317 htmlHead,
318 htmlBody,
319 htmlNav,
320 htmlHeader,
321 htmlFooter,
322 htmlMessages,
323 renderNavLink,
324 LI,
325 UL,
326 OL,
327 htmlPage,
328 };