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

100% Statements 47/47
100% Branches 20/20
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                4x 4x     4x 4x     4x               298x 4x   294x 294x             294x                 49x 31x   18x 18x 18x             30x               21x                 23x                             30x     30x 59x 57x   2x       30x 152x 149x   3x     30x                       21x     21x         21x               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;
}