import { format } from 'date-fns' import * as jdenticon from 'jdenticon' import { Role } from '../../generated/prisma' import { isModerator, hasRole } from './privs' import { signUrl } from './cdn' import { extractBasePath } from './filesystem' import { formatDuration, truncate } from './formatters.ts' import { icons } from './icons.ts' import logger from './logger.ts' import HandlebarsLib, { HelperOptions, SafeString } from 'handlebars'; import { env } from '../config/env.ts' import { basename } from 'node:path' import { constants } from '../config/constants.ts' export function registerHbsHelpers(Handlebars: typeof HandlebarsLib) { Handlebars.registerHelper('formatDate', function (dateString) { if (!dateString) return '' return format(new Date(dateString), 'yyyy-MM-dd') }) Handlebars.registerHelper('formatDuration', function (duration) { return formatDuration(duration) }) Handlebars.registerHelper('identicon', function (str, size = 48) { return jdenticon.toSvg(str, size) }) Handlebars.registerHelper('safeJson', function (context) { return new Handlebars.SafeString(JSON.stringify(context)); }); Handlebars.registerHelper('json', function (context) { return JSON.stringify(context) }) Handlebars.registerHelper('patron', function (user) { if (!user.roles) { throw new Error( 'patron hbs helper was called without roles. This usually means you forgot to include roles relationship in the query.' ); } return user.roles.some((r: Role) => r.name.startsWith('supporter')); }); Handlebars.registerHelper('notEqual', function (a, b) { return a !== b; }); Handlebars.registerHelper('isEqual', function (a, b) { logger.trace(`isEqual a=${a} b=${b}`) return a == b }); Handlebars.registerHelper('isModerator', function (user) { return isModerator(user) }) Handlebars.registerHelper('hasRole', hasRole) Handlebars.registerHelper('breaklines', function (text) { text = Handlebars.Utils.escapeExpression(text); text = text.replace(/(\r\n|\n|\r)/gm, '
'); return new Handlebars.SafeString(text); }); Handlebars.registerHelper('getCdnUrl', function (s3Key) { // Before you remove this log, find a way to memoize this function! logger.info(`getCdnUrl called with CDN_ORIGIN=${env.CDN_ORIGIN} and CDN_TOKEN_SECRET=${env.CDN_TOKEN_SECRET}`) return signUrl(`${env.CDN_ORIGIN}/${s3Key}`, { securityKey: env.CDN_TOKEN_SECRET, expirationTime: constants.timeUnits.sevenDaysInSeconds, }) }) /** * @see https://github.com/video-dev/hls.js/issues/2152 */ Handlebars.registerHelper('signedHlsUrl', function (s3Key) { if (!s3Key) throw new Error(`signedHlsUrl called with falsy s3Key=${s3Key}`); const pathAllowed = extractBasePath(s3Key) const url = signUrl(`${env.CDN_ORIGIN}/${s3Key}`, { securityKey: env.CDN_TOKEN_SECRET, pathAllowed, isDirectory: true, expirationTime: constants.timeUnits.sevenDaysInSeconds, }) logger.debug(`pathAllowed=${pathAllowed} url=${url}`) return url }) Handlebars.registerHelper('basename', function (url: string) { return basename(url) }) Handlebars.registerHelper('trunc', function (str, length = 6) { return truncate(str, length) }); Handlebars.registerHelper('icon', function ( name: string, size = 20, color?: string, options?: any ) { // Handle case where color is omitted but options object is passed if (typeof color === 'object' && color !== null && 'hash' in color) { options = color; color = undefined; } const svg = icons[name]; if (!svg) { return new Handlebars.SafeString(``); } const finalColor = color || 'currentColor'; // Remove any existing width, height, and fill attributes let sizedSvg = svg .replace(/\s(width|height|fill)="[^"]*"/g, '') // Inject our width, height, and fill .replace(/]*)>/, ``); return new Handlebars.SafeString(sizedSvg); }); }