bump package version to 1.6.1
[squeep-html-template-helper] / lib / template-helper.js
index 7e51bcfbb9058a25287bf934978d9234b368bf80..f2f4efe1478e153a12c2f2a42c6504267fc3f2e2 100644 (file)
@@ -10,7 +10,7 @@ const { lazy } = require('@squeep/lazy-property');
 /**
  * Set up expected fields for how we handle error reporting
  * and whatnot.
- * @param {Object} ctx
+ * @param {object} ctx context
  */
 const initContext = (ctx) => {
   ctx.errors = [];
@@ -19,8 +19,9 @@ const initContext = (ctx) => {
 
 /**
  * Some fields may have values outside normal dates, handle them here.
- * @param {Date} date
- * @param {String} otherwise
+ * @param {Date|number} date potential Date or epoch milliseconds
+ * @param {string} otherwise returned if date is not a date
+ * @returns {string} date or otherwise
  */
 const dateOrNot = (date, otherwise) => {
   if (!date) {
@@ -42,10 +43,11 @@ const dateOrNot = (date, otherwise) => {
  * Why is rendering a Date as a string this complicated?
  * 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
+ * @param {Date|number|string} date Date, epoch milliseconds, or date string
+ * @param {string=} pInf returned if date is +Infinity
+ * @param {string=} nInf returned if date is -Infinity
+ * @param {string=} otherwise returned if date is something else
+ * @returns {string} rendered date
  */
 const dateFormat = (date, pInf = 'Never', nInf = 'Forever', otherwise = '') => {
   const isDatableType = ['number', 'string'].includes(typeof date);
@@ -77,10 +79,11 @@ lazy(dateFormat, '_dtf', () => {
 
 
 /**
- * Wrap a Date in a <time> block.
- * @param {Date} date
- * @param {Object} options
- * @param {String=} options.title
+ * Wrap a Date in a <time> element block.
+ * @param {Date} date date
+ * @param {object} options options
+ * @param {string=} options.title title attr for time element
+ * @returns {string} time element
  */
 const timeElement = (date, options = {}) => {
   const {
@@ -105,8 +108,8 @@ const timeElement = (date, options = {}) => {
 
 /**
  * Render a duration.
- * @param {Number} seconds
- * @returns {String}
+ * @param {number} seconds duration
+ * @returns {string} readable duration
  */
 const secondsToPeriod = (seconds) => {
   let value = seconds;
@@ -133,8 +136,9 @@ const secondsToPeriod = (seconds) => {
 
 /**
  * Return array of strings prefixed with tabs.
- * @param {Number} indent
- * @param {String[]} list
+ * @param {number} indent depth
+ * @param {string[]} list items to indent by depth
+ * @returns {string[]} indented items
  */
 const indented = (indent, list) => {
   const spacer = '\t'.repeat(indent);
@@ -144,12 +148,12 @@ const indented = (indent, list) => {
 
 /**
  * Render the preamble <head> for an HTML page.
- * @param {Number} pagePathLevel number of paths below root this page is
- * @param {Object} ctx
- * @param {Object} options
- * @param {String[]=} options.headElements
- * @param {String=} options.pageTitle
- * @returns {String}
+ * @param {number} pagePathLevel number of paths below root this page is
+ * @param {object} ctx context
+ * @param {object} options options
+ * @param {string[]=} options.headElements element strings to add to html head element
+ * @param {string=} options.pageTitle title of page
+ * @returns {string} populated head element
  */
 function htmlHead(pagePathLevel, ctx, options) {
   const rootPathPfx = '../'.repeat(pagePathLevel);
@@ -170,12 +174,12 @@ ${headElements.map((e) => '\t\t' + e).join('\n')}
 
 /**
  * Render the main content of an HTML page.
- * @param {Number} pagePathLevel
- * @param {Object} ctx
- * @param {Object} options
- * @param {Object} options.bodyAttributes
- * @param {String[]} main
- * @returns {String}
+ * @param {number} pagePathLevel number of subdirs under root this page is
+ * @param {object} ctx context
+ * @param {object} options options
+ * @param {object} options.bodyAttributes attributes to set on body element
+ * @param {string[]} main main content strings
+ * @returns {string} body element
  */
 function htmlBody(pagePathLevel, ctx, options, main = []) {
   const {
@@ -196,23 +200,24 @@ ${htmlFooter(ctx, options)}
 
 /**
  * Render a navigation link for the header section.
- * @param {Object} nav
- * @param {String} nav.href
- * @param {String} nav.class
- * @param {String} nav.text
- * @returns {String}
+ * @param {object} nav navigation object
+ * @param {string} nav.href href of link
+ * @param {string} nav.class class for anchor element
+ * @param {string} nav.text text of link
+ * @returns {string} a element
  */
 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>`;
 }
 
 
 /**
  * Render the navigation section of the page header.
- * @param {Object} ctx
- * @param {Object} options
- * @param {Object[]=} options.navLinks
- * @returns {String}
+ * @param {object} ctx context
+ * @param {object} options options
+ * @param {object[]=} options.navLinks array of navlink objects
+ * @returns {string} nav element
  */
 function htmlNav(ctx, options) {
   const indent = 3;
@@ -228,20 +233,22 @@ ${spacer}</nav>` : '';
 
 /**
  * Render the banner and navigation header.
- * @param {Number} pagePathLevel
- * @param {Object} ctx
- * @param {Object} options
- * @param {String[]=} options.logoUrl
- * @param {String[]=} options.pageTitle
- * @returns {String}
+ * @param {number} pagePathLevel number of subdirs under root this page is
+ * @param {object} ctx context
+ * @param {object} options options
+ * @param {string[]=} options.logoUrl url to logo image
+ * @param {string[]=} options.logoAlt alt for logo image
+ * @param {string[]=} options.pageTitle page title
+ * @returns {string} header element
  */
 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)}
@@ -251,10 +258,10 @@ ${htmlNav(ctx, options)}
 
 /**
  * Render the bottom boilerplate.
- * @param {Object} ctx
- * @param {Object} options
- * @param {String[]} options.footerEntries
- * @returns {String}
+ * @param {object} ctx context
+ * @param {object} options options
+ * @param {string[]} options.footerEntries footer entries
+ * @returns {string} footer element
  */
 function htmlFooter(ctx, options) {
   const indent = 2;
@@ -271,21 +278,24 @@ ${spacer}</footer>` : '';
 
 /**
  * Convert an object into element attributes.
- * @param {Object} attributes
- * @returns {String}
+ * @param {object} attributes attributes
+ * @returns {string} formatted attributes
  */
 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 : '';
 }
 
 
 /**
  * Wrap an item in a list item element.
- * @param {String} item
- * @param {Number} indent
- * @param {Object} attributes
- * @returns {String}
+ * @param {string} item text of item
+ * @param {number} indent indent level
+ * @param {object} attributes attributes for li element
+ * @returns {string} li element
  */
 function LI(item, indent = 0, attributes = {}) {
   const spacer = '\t'.repeat(indent);
@@ -294,60 +304,87 @@ function LI(item, indent = 0, attributes = {}) {
 
 
 /**
- * Wrap a list of items in an unordered list.
- * @param {String[]} items
- * @param {Number} indent
- * @param {Object} attributes
- * @param {(item) => Object} itemAttributeGenerator
- * @returns {String}
+ * @typedef {(item: any, index: number, array: any[]) => object} ItemAttributeGenerator
  */
-function UL(items, indent = 0, attributes = {}, itemAttributeGenerator = () => {}) {
+
+/**
+ * Wrap an array of items in a list container element.
+ * @param {string} element element type, e.g. OL, UL
+ * @param {number} indent indent level
+ * @param {object} attributes attributes for list element
+ * @param {string[]} items list entry elements
+ * @param {ItemAttributeGenerator} itemAttributeGenerator function which returns attributes for a given list entry element
+ * @returns {string} list type element
+ */
+function listContainer(element, indent, attributes, items, 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 `${spacer}<${element}${elementAttributes(attributes)}>
+${items.map((item, index, array) => LI(item, indent + 1, itemAttributeGenerator(item, index, array))).join('\n')}
+${spacer}</${element}>`;
 }
 
 
+// eslint-disable-next-line jsdoc/require-returns-check
 /**
- * Wrap a list of items in an ordered list.
- * @param {String[]} items
- * @param {Number} indent
- * @param {Object} attributes
- * @param {(item) => Object} itemAttributeGenerator
- * @returns {String}
+ * @param {any} item item
+ * @param {number} index item index
+ * @param {Array<any>} array items
+ * @returns {object=} attribute map
  */
+function _defaultItemAttributeGenerator(item, index, array) { // eslint-disable-line no-unused-vars
+  return;
+}
 
-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>`;
+
+/**
+ * Wrap a list of items in an unordered list.
+ * @param {string[]} items list item elements
+ * @param {number} indent indent level
+ * @param {object} attributes ul element attributes
+ * @param {ItemAttributeGenerator} itemAttributeGenerator function which returns attributes for a given list entry element
+ * @returns {string} ul element
+ */
+function UL(items, indent = 0, attributes = {}, itemAttributeGenerator = _defaultItemAttributeGenerator) {
+  return listContainer('ul', indent, attributes, items, itemAttributeGenerator);
+}
+
+
+/**
+ * Wrap a list of items in an ordered list.
+ * @param {string[]} items list item elements
+ * @param {number} indent indent level
+ * @param {object} attributes ol element attributes
+ * @param {ItemAttributeGenerator} itemAttributeGenerator function which returns attributes for a given liste entry element
+ * @returns {string} ol element
+ */
+function OL(items, indent = 0, attributes = {}, itemAttributeGenerator = _defaultItemAttributeGenerator) {
+  return listContainer('ol', indent, attributes, items, itemAttributeGenerator);
 }
 
 
 /**
  * Show any error or notice messages from context.
- * @param {Object} ctx
- * @param {String[]=} ctx.errors
- * @param {String[]=} ctx.notifications
- * @param {Object} options
- * @param {String=} options.errorHeading
- * @param {String[]=} options.errorContent
- * @param {String=} options.notificationHeading
- * @param {String[]=} options.notificationContent
+ * @param {object} ctx context
+ * @param {string[]=} ctx.errors errors to render
+ * @param {string[]=} ctx.notifications notifications to render
+ * @param {object} options options
+ * @param {string=} options.errorHeading additional heading when rendering errors
+ * @param {string[]=} options.errorContent additional text when rendering errors
+ * @param {string=} options.notificationHeading additional heading when rendering notifications
+ * @param {string[]=} options.notificationContent additional text when rendering notifications
+ * @returns {string} h2 element and section element 
  */
 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>` : '';
@@ -356,42 +393,27 @@ ${UL(ctx.notifications, 1)}
 
 
 /**
- * Render all parts of an HTML page. Adds user logout nav link automatically.
- * @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 {Object[]=} options.bodyAttributes
- * @param {String[]=} options.headElements
- * @param {Object[]=} options.navLinks
- * @param {String[]=} options.footerEntries
- * @param {String=} options.errorHeading
- * @param {String[]=} options.errorContent
- * @param {String=} options.notificationHeading
- * @param {String[]=} options.notificationContent
- * @param {String[]} main - contents
- * @returns {String}
+ * Render all parts of an HTML page.
+ * @param {number} pagePathLevel relative path-distance to base
+ * @param {object} ctx context
+ * @param {string[]=} ctx.errors errors
+ * @param {string[]=} ctx.notifications notifications
+ * @param {object} options options
+ * @param {string=} options.pageTitle page title
+ * @param {string=} options.logoUrl logo url
+ * @param {string=} options.logoAlt logo alt
+ * @param {object[]=} options.bodyAttributes body attributes
+ * @param {string[]=} options.headElements head element elements
+ * @param {object[]=} options.navLinks nav objects
+ * @param {string[]=} options.footerEntries footers
+ * @param {string=} options.errorHeading errors heading
+ * @param {string[]=} options.errorContent errors text
+ * @param {string=} options.notificationHeading notifications heading
+ * @param {string[]=} options.notificationContent notifications text
+ * @param {string[]} main array of contents strings
+ * @returns {string} html page
  */
 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">',
@@ -417,7 +439,9 @@ module.exports = {
   indented,
   renderNavLink,
   LI,
+  listContainer,
   UL,
   OL,
   htmlPage,
+  elementAttributes,
 };