update devDependencies, eslint config
[squeep-html-template-helper] / lib / template-helper.js
index c61958db9a31a67ae34b84dea72edfa6a4e125f0..7e51bcfbb9058a25287bf934978d9234b368bf80 100644 (file)
@@ -4,6 +4,19 @@
  * A bunch of shorthand to put together common parts of an HTML page. 
  */
 
+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
@@ -25,6 +38,71 @@ 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
+ */
+const dateFormat = (date, pInf = 'Never', nInf = 'Forever', otherwise = '') => {
+  const isDatableType = ['number', 'string'].includes(typeof date);
+  switch (date) {
+    case Infinity:
+      return pInf;
+    case -Infinity:
+      return nInf;
+    default:
+      if (!date
+      ||  Number.isNaN(date.valueOf())
+      ||  (!(date instanceof Date) && !isDatableType)) {
+        return otherwise;
+      }
+  }
+  if (isDatableType) {
+    date = new Date(date);
+  }
+  const parts = dateFormat._dtf.formatToParts(date);
+  return parts.map((p) => p.value).join('');
+};
+lazy(dateFormat, '_dtf', () => {
+  const dateTimeFormatOptions = {
+    dateStyle: 'medium',
+    timeStyle: 'long',
+  };
+  return new Intl.DateTimeFormat(undefined, dateTimeFormatOptions);
+});
+
+
+/**
+ * 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
@@ -40,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');
@@ -53,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
@@ -314,7 +403,10 @@ function htmlPage(pagePathLevel, ctx, options, main = []) {
 
 
 module.exports = {
+  initContext,
   dateOrNot,
+  dateFormat,
+  timeElement,
   secondsToPeriod,
   htmlHead,
   htmlBody,
@@ -322,6 +414,7 @@ module.exports = {
   htmlHeader,
   htmlFooter,
   htmlMessages,
+  indented,
   renderNavLink,
   LI,
   UL,