roadmap-gen
Version:
Professional HTML roadmap generator from YAML data with multiple themes
120 lines (108 loc) • 3.5 kB
text/typescript
/**
* HTML generation utilities to reduce code duplication
*/
import type { DetailEntry, MetricEntry } from '../core/types.ts';
// ASCII code for apostrophe character to avoid quote conflicts in ESLint/Prettier
const APOSTROPHE_CHAR_CODE = 39;
/**
* Escapes HTML special characters to prevent XSS
*/
export function escapeHtml(text: string): string {
const htmlEscapes: Record<string, string> = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
'/': '/',
[String.fromCharCode(APOSTROPHE_CHAR_CODE)]: ''',
};
return text.replace(/[&<>"'/]/g, char => htmlEscapes[char] || char);
}
/**
* Validates and sanitizes URLs
*/
export function sanitizeUrl(url: string): string {
// Only allow http, https, and relative URLs
const sanitized = url.trim();
if (sanitized.startsWith('javascript:') || sanitized.startsWith('data:') || sanitized.startsWith('vbscript:')) {
return '#';
}
return sanitized;
}
/**
* Generates HTML list items from an array of strings
*
* @param items - Array of string items
* @param itemClass - CSS class for each item
* @param prefix - Optional prefix for each item (e.g., '• ')
* @returns HTML string with list items
*/
export function generateListItems(items: string[], itemClass: string, prefix = ''): string {
return items.map(item => `<div class="${itemClass}">${prefix}${escapeHtml(item)}</div>`).join('');
}
/**
* Generates a section with title and content
*
* @param title - Section title
* @param content - Section content HTML
* @param sectionClass - CSS class for the section
* @returns HTML section
*/
export function generateSection(title: string, content: string, sectionClass: string): string {
return `
<div class="${sectionClass}">
<h3>${title}</h3>
${content}
</div>`;
}
/**
* Creates info sections with label and content
*
* @param label - Label text (e.g., 'Progress:', 'Metrics:')
* @param content - Content to display
* @param infoClass - CSS class for the info section
* @returns HTML info section
*/
export function createInfoSection(label: string, content: string, infoClass: string): string {
return `<div class="${infoClass}"><strong>${escapeHtml(label)}</strong> ${escapeHtml(content)}</div>`;
}
/**
* Creates info section with list content
*
* @param label - Label text
* @param items - Array of items to list
* @param infoClass - CSS class for the info section
* @returns HTML info section with list
*/
export function createInfoList(label: string, items: string[], infoClass: string): string {
const itemsList = items.map(item => `• ${escapeHtml(item)}`).join('<br>');
return `<div class="${infoClass}"><strong>${escapeHtml(label)}</strong><br>${itemsList}</div>`;
}
/**
* Converts DetailEntry array to string array for HTML generation
*
* @param details - Array of DetailEntry (string or DetailItem)
* @returns Array of strings
*/
export function detailEntriesToStrings(details: DetailEntry[]): string[] {
return details.map(detail => {
if (typeof detail === 'string') {
return detail;
}
return detail.text;
});
}
/**
* Converts MetricEntry array to string array for HTML generation
*
* @param metrics - Array of MetricEntry (string or MetricItem)
* @returns Array of strings
*/
export function metricEntriesToStrings(metrics: MetricEntry[]): string[] {
return metrics.map(metric => {
if (typeof metric === 'string') {
return metric;
}
return metric.text;
});
}