/**
* 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 = true;
}
// Add list item
const itemContent = line.substring(2);
listHtml += `- ${itemContent}
`;
} else if (line.match(/^\d+\. .+/)) {
if (!inList || !listHtml.includes('')) {
// Close any open unordered list
if (inList && listHtml.includes('';
}
// Start a new ordered list
listHtml += '';
inList = true;
}
// Add ordered list item
const itemContent = line.replace(/^\d+\. /, '');
listHtml += `- ${itemContent}
`;
} else if (inList) {
// End the list
if (listHtml.includes('')) {
listHtml += '
';
} else {
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];
});
}
}
};
}