All files / packages/design-system/scripts/validation/lib template-renderer.js

100% Statements 34/34
100% Branches 14/14
100% Functions 12/12
100% Lines 33/33

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                2x 2x     2x 2x     2x               121x 4x   117x 117x             117x                 29x 14x   15x 15x 15x             30x               11x                 13x                             20x     20x 32x 30x   2x       20x 84x 81x   3x     20x                       11x     11x         11x               1x               1x    
/**
 * Simple template renderer for validation reports
 * Supports {{placeholder}} for escaped values and {{{placeholder}}} for raw HTML
 */
import fs from 'fs-extra';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
 
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
 
// Paths to assets
const stylesDir = path.resolve(__dirname, '../styles');
const templatesDir = path.resolve(__dirname, '../templates');
 
// Cache for loaded files
const cache = new Map();
 
/**
 * Escape HTML special characters
 * @param {string} text - Text to escape
 * @returns {string} Escaped text
 */
export function escapeHtml(text) {
  if (text === null || text === undefined) {
    return '';
  }
  const str = String(text);
  const map = {
    '&': '&',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#039;',
  };
  return str.replace(/[&<>"']/g, (m) => map[m]);
}
 
/**
 * Load a file from disk (with caching)
 * @param {string} filePath - Path to file
 * @returns {string} File contents
 */
export function loadFile(filePath) {
  if (cache.has(filePath)) {
    return cache.get(filePath);
  }
  const content = fs.readFileSync(filePath, 'utf8');
  cache.set(filePath, content);
  return content;
}
 
/**
 * Clear the file cache (useful for testing)
 */
export function clearCache() {
  cache.clear();
}
 
/**
 * Load the shared CSS styles
 * @returns {string} CSS content
 */
export function loadStyles() {
  return loadFile(path.join(stylesDir, 'report.css'));
}
 
/**
 * Load a template file
 * @param {string} templateName - Template filename (e.g., 'index.html')
 * @returns {string} Template content
 */
export function loadTemplate(templateName) {
  return loadFile(path.join(templatesDir, templateName));
}
 
/**
 * Render a template with data
 *
 * Supports two placeholder syntaxes:
 * - {{key}} - HTML-escaped value
 * - {{{key}}} - Raw HTML (unescaped)
 *
 * @param {string} template - Template string
 * @param {Object} data - Data object with values
 * @returns {string} Rendered HTML
 */
export function render(template, data) {
  let result = template;
 
  // First, handle raw/unescaped placeholders {{{key}}}
  result = result.replace(/\{\{\{(\w+)\}\}\}/g, (match, key) => {
    if (key in data) {
      return String(data[key] ?? '');
    }
    return match; // Keep placeholder if key not found
  });
 
  // Then, handle escaped placeholders {{key}}
  result = result.replace(/\{\{(\w+)\}\}/g, (match, key) => {
    if (key in data) {
      return escapeHtml(data[key]);
    }
    return match; // Keep placeholder if key not found
  });
 
  return result;
}
 
/**
 * Load and render a template with data
 * Automatically injects styles if {{{styles}}} placeholder exists
 *
 * @param {string} templateName - Template filename
 * @param {Object} data - Data object with values
 * @returns {string} Rendered HTML
 */
export function renderTemplate(templateName, data) {
  const template = loadTemplate(templateName);
 
  // Inject styles if needed
  const dataWithStyles = {
    ...data,
    styles: data.styles ?? loadStyles(),
  };
 
  return render(template, dataWithStyles);
}
 
/**
 * Get the templates directory path
 * @returns {string} Templates directory path
 */
export function getTemplatesDir() {
  return templatesDir;
}
 
/**
 * Get the styles directory path
 * @returns {string} Styles directory path
 */
export function getStylesDir() {
  return stylesDir;
}