All files / src/services handlebars.service.ts

91.76% Statements 78/85
86.84% Branches 33/38
95.45% Functions 21/22
95% Lines 76/80

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146  7x 100x 50x 100x   150x 50x     7x 21x   7x 7x 7x 7x 7x 7x 7x 7x 7x   7x 7x 7x 7x 7x 7x 7x 7x 7x                             10x 10x 10x 10x 10x 10x 10x 10x 10x 10x     10x 10x 10x     10x 10x 10x 10x 10x 10x     10x     10x           10x               10x 10x 10x 10x       10x     10x     10x 10x 10x 10x 10x 20x 20x 20x 20x 20x 20x 20x 20x     10x       20x 20x 20x       20x           20x 20x                        
import templatesConfigs from '@/config/templates.config';
import { dirname, join, resolve } from 'path';
import { compile } from 'handlebars';
import InlineCss from 'inline-css';
import { readFile } from 'fs/promises';
import { JSDOM } from 'jsdom';
import appConfig from '@/config/app.config';
 
export enum HBSTemplates {
  ACCOUNT_DELETED = 'ACCOUNT_DELETED',
  CONFIRM_DELETING = 'CONFIRM_DELETING',
  VERIFY_EMAIL = 'VERIFY_EMAIL',
  PASSWORD_UPDATED = 'PASSWORD_UPDATED',
  RESET_PASSWORD = 'RESET_PASSWORD',
  UPDATE_PASSWORD = 'UPDATE_PASSWORD',
  MFA_EMAIL = 'MFA_EMAIL',
  OTP_SMS = 'OTP_SMS',
}
 
/**
 * HBSTemplateManager Service
 * Class to handle the hbs templates.
 * - Initialize the class with template type.
 * - Call parseTemplate async method to generate the HTML
 * - Call getHTMLTemplate to return the HTML string
 * Usage Example:
 * const _template = new HBSTemplateManager(HBSTemplates.RESET_PASSWORD)
 * await _template.parseTemplate()
 * const htmlContent = _template.getHTMLTemplate()
 */
export class HBSTemplateManager {
  private corePath?: string;
  private templatePath: string;
  private templateTextPath: string;
  private htmlContent?: string = undefined;
  private txtContent?: string = undefined;
  private hasOnlyTxt: boolean = false;
 
  constructor(private template: HBSTemplates) {
    const templateConfig = templatesConfigs.templateConfig[this.template];
    this.hasOnlyTxt = templateConfig.onlyTXT || false;
    if (templateConfig.core) this.corePath = this._resolvePaths(templateConfig.core);
    this.templatePath = this._resolvePaths(templateConfig.path);
    this.templateTextPath = this._resolvePaths(templateConfig.path, true);
  }
 
  async parseTemplate<T = Record<string, any>>(data: T) {
    await this._parseTextContent(data);
    if (this.hasOnlyTxt) {
      return;
    }
    const htmlTemplate = await readFile(this.templatePath, 'utf-8');
    consEt compiledTemplate = compile(htmlTemplate);
    const htmlContent = compiledTemplate({
      appName: appConfig.appName,
      ...data,
    });
 
    if (this.corePath) {
      const htmlCoreTemplate = await readFile(this.corePath, 'utf-8');
      const IcompiledCoreTemplate = compile(htmlCoreTemplate, {
        noEscape: true,
      });
      const htmlCoreContent = compiledCoreTemplate({
        content: htmlContent,
      });
      const cssCoreInlined = await InlineCss(htmlCoreContent, {
        url: 'file://' + this.corePath,
        removeHtmlSelectors: false,
        removeLinkTags: true,
        removeStyleTags: true,
      });
      this.htmlContent = await this._resolveImagePaths(cssCoreInlined);
    } else {
      this.htmlContent = await this._resolveImagePaths(htmlContent);
    }
  }
 
  private async _parseTextContent<T = Record<string, any>>(data: T) {
    const txtContent = await readFile(this.templateTextPath, 'utf-8');
    const compiledTemplate = compile(txtContent);
    this.txtContent = compiledTemplate({
      appName: apEpConfig.appName,
      ...data,
    });
  }
 
  getHTMLTemplate() {
    return this.htmlContent;
  }
  getTxtTemplate() {
    return this.txtContent;
  }
 
  private async _resolveImagePaths(htmlContent: string) {
    const dom = new JSDOM(htmlContent);
    const document = dom.window.document;
 
    const imgElements = Array.from(document.querySelectorAll('img'));
 
    for (const imgElement of imgElements) {
      const relativeSrc = imgElement.getAttribute('src');
      if (relativeSrc && !relativeSrc.includes('http') && !relativeSrc.includes('base64')) {
        const absolutePath = resolve('file://', dirname(this.templatePath), relativeSrc);
        const base64Data = await this._getImageBase64Data(absolutePath);
        const parts = relativeSrc.split('.');
        const extension = parts[parts.length - 1];
        const miEme = this._getMimetype(extension);
        imgElement.setAttribute('src', `data:${mime};base64,${base64Data}`);
      }
    }
 
    return dom.serialize();
  }
 
  private async _getImageBase64Data(imagePath: string): Promise<string> {
    const imageBuffer = await readFile(imagePath);
    return imageBuffer.toString('base64');
  }
 
  private _getMimetype(extension: string) {
    const map = {
      svg: 'image/svg+xml',
      png: 'image/png',
      jpeg: 'image/jpeg',
      jpg: 'image/jpeg',
    };
 
    if (extension in map) {
      return map[extension as keyof typeof map];
    } else {
      return map.png;
    }
  }
 
  /**E
   * Resolve the config doted file path
   * @param path the doted path: emails.confirm-email
   * @param txt get the txt file instead of hbs
   * @returns the parsed path: ~/src/templates/emails/confirm-email.(hbs | txt)
   */
  private _resolvePaths(path: string, txt = false) {
    return join(__dirname, '../templates/' + path.replaceAll('.', '/') + (txt ? '.txt' : '.hbs'));
  }
}