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