minor refactors
[squeep-html-template-helper] / lib / template-helper.js
index 7e1693c857758c127d1063d30804c6c5fab9b2e6..e8163abbe41733c0e6af0293b49cf22102a98178 100644 (file)
@@ -4,11 +4,22 @@
  * A bunch of shorthand to put together common parts of an HTML page. 
  */
 
-const { lazy } = require('@squeep/lazy');
+const { lazy } = require('@squeep/lazy-property');
+
+
+/**
+ * Set up expected fields for how we handle error reporting
+ * and whatnot.
+ * @param {Object} ctx
+ */
+const initContext = (ctx) => {
+  ctx.errors = [];
+  ctx.notifications = [];
+};
 
 /**
  * Some fields may have values outside normal dates, handle them here.
- * @param {Date} date
+ * @param {Date|Number} date
  * @param {String} otherwise
  */
 const dateOrNot = (date, otherwise) => {
@@ -29,7 +40,9 @@ const dateOrNot = (date, otherwise) => {
 
 /**
  * Why is rendering a Date as a string this complicated?
- * @param {Date|Number} date
+ * We handle the infinities because pg-promise might provide those in
+ * lieu of a Date object from timestamp fields.
+ * @param {Date|Number|String} date
  * @param {String=} pInf
  * @param {String=} nInf
  * @param {String=} otherwise
@@ -43,6 +56,7 @@ const dateFormat = (date, pInf = 'Never', nInf = 'Forever', otherwise = '') => {
       return nInf;
     default:
       if (!date
+      ||  Number.isNaN(date.valueOf())
       ||  (!(date instanceof Date) && !isDatableType)) {
         return otherwise;
       }
@@ -62,6 +76,33 @@ lazy(dateFormat, '_dtf', () => {
 });
 
 
+/**
+ * Wrap a Date in a <time> block.
+ * @param {Date} date
+ * @param {Object} options
+ * @param {String=} options.title
+ */
+const timeElement = (date, options = {}) => {
+  const {
+    title,
+    pInf,
+    nInf,
+    otherwise,
+  } = options;
+  const attributes = {
+    ...(title && { title }),
+    ...(date instanceof Date && { datetime: date.toISOString() }),
+  };
+  return [
+    '<time',
+    elementAttributes(attributes),
+    '>',
+    dateFormat(date, pInf, nInf, otherwise),
+    '</time>',
+  ].join('');
+};
+
+
 /**
  * Render a duration.
  * @param {Number} seconds
@@ -77,7 +118,7 @@ const secondsToPeriod = (seconds) => {
       result.push(`${r} ${label}${r != 1 ? 's' : ''}`);
     }
     value = factor ? Math.floor(value / factor) : value;
-  }
+  };
 
   nextResult(60, 'second');
   nextResult(60, 'minute');
@@ -90,6 +131,17 @@ const secondsToPeriod = (seconds) => {
 };
 
 
+/**
+ * Return array of strings prefixed with tabs.
+ * @param {Number} indent
+ * @param {String[]} list
+ */
+const indented = (indent, list) => {
+  const spacer = '\t'.repeat(indent);
+  return list.map((l) => `${spacer}${l}`);
+};
+
+
 /**
  * Render the preamble <head> for an HTML page.
  * @param {Number} pagePathLevel number of paths below root this page is
@@ -108,8 +160,8 @@ function htmlHead(pagePathLevel, ctx, options) {
   return `\t<head>
 \t\t<meta charset="utf-8">
 \t\t<meta name="viewport" content="width=device-width,initial-scale=1">
-\t\t<link rel="stylesheet" href="${rootPathPfx}static/theme.css" title="Default">
-\t\t<link rel="stylesheet" href="${rootPathPfx}static/custom.css" title="Site Specific">
+\t\t<link rel="stylesheet" href="${rootPathPfx}static/theme.css">
+\t\t<link rel="stylesheet" href="${rootPathPfx}static/custom.css">
 ${headElements.map((e) => '\t\t' + e).join('\n')}
 \t\t<title>${pageTitle}</title>
 \t</head>`;
@@ -151,7 +203,8 @@ ${htmlFooter(ctx, options)}
  * @returns {String}
  */
 function renderNavLink(nav) {
-  return `<a href="${nav.href}"${nav.class ? (' class="' + nav.class + '"') : ''}>${nav.text}</a>`;
+  const aClass = nav.class ? ` class="${nav.class}"` : '';
+  return `<a href="${nav.href}"${aClass}>${nav.text}</a>`;
 }
 
 
@@ -180,6 +233,7 @@ ${spacer}</nav>` : '';
  * @param {Object} ctx
  * @param {Object} options
  * @param {String[]=} options.logoUrl
+ * @param {String[]=} options.logoAlt
  * @param {String[]=} options.pageTitle
  * @returns {String}
  */
@@ -187,9 +241,10 @@ function htmlHeader(pagePathLevel, ctx, options) {
   const rootPathPfx = '../'.repeat(pagePathLevel);
   const {
     logoUrl = '',
+    logoAlt = 'logo',
     pageTitle = '',
   } = options;
-  const logoImg = logoUrl ? `<img src="${rootPathPfx}${logoUrl}" class="logo">` : '';
+  const logoImg = logoUrl ? `<img src="${rootPathPfx}${logoUrl}" alt="logo" class="${logoAlt}">` : '';
   return `\t\t<header>
 \t\t\t<h1>${logoImg}${pageTitle}</h1>
 ${htmlNav(ctx, options)}
@@ -223,7 +278,10 @@ ${spacer}</footer>` : '';
  * @returns {String}
  */
 function elementAttributes(attributes) {
-  const attr = Object.entries(attributes).map(([name, value]) => `${name}="${value}"`).join(' ');
+  const attr = Object.entries(attributes).map(([name, value]) => {
+    const v = value ? `="${value}"` : '';
+    return `${name}${v}`;
+  }).join(' ');
   return attr ? ' ' + attr : '';
 }
 
@@ -241,19 +299,33 @@ function LI(item, indent = 0, attributes = {}) {
 }
 
 
+/**
+ * Wrap an array of items in a list container element.
+ * @param {String} element
+ * @param {Number} indent
+ * @param {Object} attributes
+ * @param {String[]} items
+ * @param {(item, index, array) => {Object}} itemAttributeGenerator
+ * @returns {String}
+ */
+function listContainer(element, indent, attributes, items, itemAttributeGenerator) {
+  const spacer = '\t'.repeat(indent);
+  return `${spacer}<${element}${elementAttributes(attributes)}>
+${items.map((item, index, array) => LI(item, indent + 1, itemAttributeGenerator(item, index, array))).join('\n')}
+${spacer}</${element}>`;
+}
+
+
 /**
  * Wrap a list of items in an unordered list.
  * @param {String[]} items
  * @param {Number} indent
  * @param {Object} attributes
- * @param {(item) => Object} itemAttributeGenerator
+ * @param {(item, index, array) => Object} itemAttributeGenerator
  * @returns {String}
  */
 function UL(items, indent = 0, attributes = {}, itemAttributeGenerator = () => {}) {
-  const spacer = '\t'.repeat(indent);
-  return `${spacer}<ul${elementAttributes(attributes)}>
-${items.map((item) => LI(item, indent + 1, itemAttributeGenerator(item))).join('\n')}
-${spacer}</ul>`;
+  return listContainer('ul', indent, attributes, items, itemAttributeGenerator);
 }
 
 
@@ -262,15 +334,12 @@ ${spacer}</ul>`;
  * @param {String[]} items
  * @param {Number} indent
  * @param {Object} attributes
- * @param {(item) => Object} itemAttributeGenerator
+ * @param {(item, index, array) => Object} itemAttributeGenerator
  * @returns {String}
  */
 
 function OL(items, indent = 0, attributes = {}, itemAttributeGenerator = () => {}) {
-  const spacer = '\t'.repeat(indent);
-  return `${spacer}<ol${elementAttributes(attributes)}>
-${items.map((item) => LI(item, indent + 1, itemAttributeGenerator(item))).join('\n')}
-${spacer}</ol>`;
+  return listContainer('ol', indent, attributes, items, itemAttributeGenerator);
 }
 
 
@@ -286,16 +355,16 @@ ${spacer}</ol>`;
  * @param {String[]=} options.notificationContent
  */
 function htmlMessages(ctx, options) {
-  const errorHeading = options && options.errorHeading ? `
+  const errorHeading = options?.errorHeading ? `
 \t<h2>${options.errorHeading}</h2>` : '';
-  const errorContent = options && options.errorContent && options.errorContent.length ? '\n' + options.errorContent.map(((content) => `\t${content}`)).join('\n') : '';
-  const notificationHeading = options && options.notificationHeading ? `\n\t<h2>${options.notificationHeading}</h2>` : '';
-  const notificationContent = options && options.notificationContent && options.notificationContent.length ? '\n' + options.notificationContent.map(((content) => `\t${content}`)).join('\n') : '';
-  const errors = ctx && ctx.errors && ctx.errors.length ? `
+  const errorContent = options?.errorContent?.length ? '\n' + options.errorContent.map(((content) => `\t${content}`)).join('\n') : '';
+  const notificationHeading = options?.notificationHeading ? `\n\t<h2>${options.notificationHeading}</h2>` : '';
+  const notificationContent = options?.notificationContent?.length ? '\n' + options.notificationContent.map(((content) => `\t${content}`)).join('\n') : '';
+  const errors = ctx?.errors?.length ? `
 <section class="error">${errorHeading}${errorContent}
 ${UL(ctx.errors, 1)}
 </section>` : '';
-  const notifications = ctx && ctx.notifications && ctx.notifications.length ? `
+  const notifications = ctx?.notifications?.length ? `
 <section class="notification">${notificationHeading}${notificationContent}
 ${UL(ctx.notifications, 1)}
 </section>` : '';
@@ -304,17 +373,15 @@ ${UL(ctx.notifications, 1)}
 
 
 /**
- * Render all parts of an HTML page. Adds user logout nav link automatically.
+ * Render all parts of an HTML page.
  * @param {Number} pagePathLevel - relative path-distance to base
  * @param {Object} ctx
- * @param {Object=} ctx.session
- * @param {String=} ctx.session.authenticatedIdentifier
- * @param {String=} ctx.session.authenticatedProfile
  * @param {String[]=} ctx.errors
  * @param {String[]=} ctx.notifications
  * @param {Object} options
  * @param {String=} options.pageTitle
  * @param {String=} options.logoUrl
+ * @param {String=} options.logoAlt
  * @param {Object[]=} options.bodyAttributes
  * @param {String[]=} options.headElements
  * @param {Object[]=} options.navLinks
@@ -327,19 +394,6 @@ ${UL(ctx.notifications, 1)}
  * @returns {String}
  */
 function htmlPage(pagePathLevel, ctx, options, main = []) {
-  const user = (ctx && ctx.session && ctx.session.authenticatedProfile) || (ctx && ctx.session && ctx.session.authenticatedIdentifier);
-  if (user) {
-    if (!options.navLinks) {
-      options.navLinks = [];
-    }
-    const logoutRedirect = ctx && ctx.url ? `?r=${encodeURIComponent(ctx.url)}` : '';
-    const logoutPath = (pagePathLevel > 0) ? `${'../'.repeat(pagePathLevel - 1)}` : 'admin/';
-    options.navLinks.push({
-      text: `Logout (${user})`,
-      href: `${logoutPath}logout${logoutRedirect}`,
-    });
-  }
-
   return [
     '<!DOCTYPE html>',
     '<html lang="en">',
@@ -351,8 +405,10 @@ function htmlPage(pagePathLevel, ctx, options, main = []) {
 
 
 module.exports = {
+  initContext,
   dateOrNot,
   dateFormat,
+  timeElement,
   secondsToPeriod,
   htmlHead,
   htmlBody,
@@ -360,9 +416,12 @@ module.exports = {
   htmlHeader,
   htmlFooter,
   htmlMessages,
+  indented,
   renderNavLink,
   LI,
+  listContainer,
   UL,
   OL,
   htmlPage,
+  elementAttributes,
 };