/** * Utility functions for the workflow module * Contains pure helper functions with no state or side effects */ /** * Formats a file size into a human-readable string * @param {number} bytes - The file size in bytes * @param {number} decimals - Number of decimal places to show * @returns {string} - Formatted file size string */ export function formatFileSize(bytes, decimals = 1) { if (bytes === 0 || bytes === null || bytes === undefined) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i]; } /** * Gets an appropriate Font Awesome icon class for a file type * @param {Object} file - File object with mimeType property * @returns {string} - Font Awesome icon class */ export function getFileTypeIcon(file) { if (!file) return 'fa-file'; // Normalize content type field name const contentType = file.mimeType; // Image files if (contentType.includes('image')) return 'fa-file-image'; // Document files if (contentType.includes('pdf')) return 'fa-file-pdf'; if (contentType.includes('msword') || contentType.includes('wordprocessingml')) return 'fa-file-word'; if (contentType.includes('spreadsheetml') || contentType.includes('excel')) return 'fa-file-excel'; if (contentType.includes('presentationml') || contentType.includes('powerpoint')) return 'fa-file-powerpoint'; // Text files if (contentType.includes('text/plain')) return 'fa-file-alt'; if (contentType.includes('text/markdown') || contentType.includes('md')) return 'fa-file-alt'; if (contentType.includes('text/csv') || contentType.includes('csv')) return 'fa-file-csv'; // Code files if (contentType.includes('javascript') || contentType.includes('typescript') || contentType.includes('json')) return 'fa-file-code'; if (contentType.includes('html') || contentType.includes('xml') || contentType.includes('css')) return 'fa-file-code'; // Archive files if (contentType.includes('zip') || contentType.includes('rar') || contentType.includes('tar') || contentType.includes('gzip')) return 'fa-file-archive'; // Audio files if (contentType.includes('audio')) return 'fa-file-audio'; // Video files if (contentType.includes('video')) return 'fa-file-video'; // Default return 'fa-file'; } /** * Formats text with markdown-like styling for rendering in the UI * @param {string} text - Text to format * @returns {string} - HTML-formatted text */ export function formatMarkdownLike(text) { if (!text) return ''; // Convert to string if not already const textStr = String(text); // Escape HTML to prevent XSS let formattedText = escapeHtml(textStr); // Format code blocks formattedText = formatCodeBlocks(formattedText); // Format inline code formattedText = formattedText.replace(/`([^`]+)`/g, '$1'); // Format bold text formattedText = formattedText.replace(/\*\*([^*]+)\*\*/g, '$1'); formattedText = formattedText.replace(/__([^_]+)__/g, '$1'); // Format italic text formattedText = formattedText.replace(/\*([^*]+)\*/g, '$1'); formattedText = formattedText.replace(/_([^_]+)_/g, '$1'); // Format links formattedText = formattedText.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); // Format headings formattedText = formattedText.replace(/^### (.+)$/gm, '

$1

'); formattedText = formattedText.replace(/^## (.+)$/gm, '

$1

'); formattedText = formattedText.replace(/^# (.+)$/gm, '

$1

'); // Format unordered lists formattedText = formatLists(formattedText); // Format line breaks formattedText = formattedText.replace(/\n/g, '
'); return formattedText; } /** * Escapes HTML special characters to prevent XSS attacks * @param {string} html - Text that might contain HTML * @returns {string} - Escaped text */ function escapeHtml(html) { const escapeMap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return html.replace(/[&<>"']/g, m => escapeMap[m]); } /** * Formats code blocks with syntax highlighting classes * @param {string} text - Text to format * @returns {string} - Text with formatted code blocks */ function formatCodeBlocks(text) { // Match triple backtick code blocks with optional language specification const codeBlockRegex = /```(\w*)\n([\s\S]+?)\n```/g; return text.replace(codeBlockRegex, (match, language, code) => { const langClass = language ? ` class="language-${language}"` : ''; const formattedCode = escapeHtml(code); return `
${formattedCode}
`; }); } /** * Formats unordered and ordered lists * @param {string} text - Text to format * @returns {string} - Text with formatted lists */ function formatLists(text) { let formatted = text; // Check if there are any list items if (formatted.match(/^[*-] .+/gm)) { // Split by newline to process lines const lines = formatted.split('\n'); let inList = false; let listHtml = ''; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.match(/^[*-] .+/)) { if (!inList) { // Start a new list listHtml += ''; } inList = false; listHtml += line + '\n'; } else { listHtml += line + '\n'; } } // Close list if still open at the end if (inList) { if (listHtml.includes('
    ')) { listHtml += '
'; } else { listHtml += ''; } } formatted = listHtml; } return formatted; } /** * Debounces a function to limit how often it can run * @param {Function} func - Function to debounce * @param {number} wait - Time to wait in milliseconds * @returns {Function} - Debounced function */ export function debounce(func, wait = 300) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } /** * Creates a throttled function that only invokes func once per wait period * @param {Function} func - The function to throttle * @param {number} wait - Milliseconds to wait between invocations * @returns {Function} - Throttled function */ export function throttle(func, wait = 300) { let lastCall = 0; return function(...args) { const now = Date.now(); if (now - lastCall >= wait) { lastCall = now; return func(...args); } }; } /** * Creates a random ID with an optional prefix * @param {string} prefix - Optional prefix for the ID * @returns {string} - Random ID */ export function generateId(prefix = '') { return `${prefix}${Date.now()}_${Math.random().toString(36).substring(2,11)}`; } /** * Deep clones an object * @param {Object} obj - Object to clone * @returns {Object} - Cloned object */ export function deepClone(obj) { if (obj === null || typeof obj !== 'object') { return obj; } // Handle Date if (obj instanceof Date) { return new Date(obj.getTime()); } // Handle Array if (Array.isArray(obj)) { return obj.map(item => deepClone(item)); } // Handle Object if (obj instanceof Object) { const copy = {}; Object.keys(obj).forEach(key => { copy[key] = deepClone(obj[key]); }); return copy; } throw new Error(`Unable to copy obj! Its type isn't supported: ${typeof obj}`); } /** * Extracts text content from HTML string * @param {string} html - HTML string * @returns {string} - Plain text content */ export function stripHtml(html) { if (!html) return ''; // Create a temporary element const tempDiv = document.createElement('div'); tempDiv.innerHTML = html; // Return the text content return tempDiv.textContent || tempDiv.innerText || ''; } /** * Checks if two objects are equal in value * @param {Object} obj1 - First object * @param {Object} obj2 - Second object * @returns {boolean} - Whether objects are equal */ export function objectEquals(obj1, obj2) { return JSON.stringify(obj1) === JSON.stringify(obj2); } /** * Converts a string to title case * @param {string} str - String to convert * @returns {string} - Title case string */ export function toTitleCase(str) { if (!str) return ''; return str.replace( /\w\S*/g, txt => txt.charAt(0).toUpperCase() + txt.substring(1).toLowerCase() ); } /** * Truncates a string to a given length * @param {string} str - String to truncate * @param {number} length - Maximum length * @param {string} suffix - Suffix to add if truncated * @returns {string} - Truncated string */ export function truncate(str, length = 50, suffix = '...') { if (!str) return ''; if (str.length <= length) { return str; } return str.substring(0, length - suffix.length) + suffix; } /** * Parses a string containing JSON and returns the parsed object * @param {string} jsonString - String to parse * @param {*} defaultValue - Default value to return if parsing fails * @returns {*} - Parsed object or default value */ export function safeJsonParse(jsonString, defaultValue = {}) { try { return JSON.parse(jsonString); } catch (e) { console.warn('Error parsing JSON:', e); return defaultValue; } } /** * Returns a formatted date string * @param {string|Date} date - Date to format * @param {string} format - Format string ('short', 'medium', 'long', 'full') * @returns {string} - Formatted date string */ export function formatDate(date, format = 'medium') { const dateObj = typeof date === 'string' ? new Date(date) : date; if (!(dateObj instanceof Date) || isNaN(dateObj)) { return ''; } switch (format) { case 'short': return dateObj.toLocaleString(undefined, { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' }); case 'time': return dateObj.toLocaleTimeString(); case 'long': return dateObj.toLocaleString(undefined, { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' }); case 'full': return dateObj.toLocaleString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }); case 'medium': default: return dateObj.toLocaleString(); } } /** * Creates an event emitter with a simple pub/sub interface * @returns {Object} - Event emitter object */ export function createEventEmitter() { const events = {}; return { /** * Subscribe to an event * @param {string} event - Event name * @param {Function} callback - Event handler * @returns {Function} - Unsubscribe function */ subscribe(event, callback) { if (!events[event]) { events[event] = []; } events[event].push(callback); // Return unsubscribe function return () => { events[event] = events[event].filter(cb => cb !== callback); }; }, /** * Publish an event with data * @param {string} event - Event name * @param {*} data - Event data */ publish(event, data) { if (!events[event]) return; events[event].forEach(callback => { callback(data); }); }, /** * Clear all subscriptions for an event * @param {string} event - Event name */ clear(event) { if (event) { delete events[event]; } else { // Clear all events if no event specified Object.keys(events).forEach(key => { delete events[key]; }); } } }; }