477 lines
No EOL
14 KiB
JavaScript
477 lines
No EOL
14 KiB
JavaScript
/**
|
|
* 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, '<code>$1</code>');
|
|
|
|
// Format bold text
|
|
formattedText = formattedText.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
formattedText = formattedText.replace(/__([^_]+)__/g, '<strong>$1</strong>');
|
|
|
|
// Format italic text
|
|
formattedText = formattedText.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
formattedText = formattedText.replace(/_([^_]+)_/g, '<em>$1</em>');
|
|
|
|
// Format links
|
|
formattedText = formattedText.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
|
|
|
// Format headings
|
|
formattedText = formattedText.replace(/^### (.+)$/gm, '<h3>$1</h3>');
|
|
formattedText = formattedText.replace(/^## (.+)$/gm, '<h2>$1</h2>');
|
|
formattedText = formattedText.replace(/^# (.+)$/gm, '<h1>$1</h1>');
|
|
|
|
// Format unordered lists
|
|
formattedText = formatLists(formattedText);
|
|
|
|
// Format line breaks
|
|
formattedText = formattedText.replace(/\n/g, '<br>');
|
|
|
|
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 `<pre><code${langClass}>${formattedCode}</code></pre>`;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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 += '<ul>';
|
|
inList = true;
|
|
}
|
|
|
|
// Add list item
|
|
const itemContent = line.substring(2);
|
|
listHtml += `<li>${itemContent}</li>`;
|
|
} else if (line.match(/^\d+\. .+/)) {
|
|
if (!inList || !listHtml.includes('<ol>')) {
|
|
// Close any open unordered list
|
|
if (inList && listHtml.includes('<ul>')) {
|
|
listHtml += '</ul>';
|
|
}
|
|
|
|
// Start a new ordered list
|
|
listHtml += '<ol>';
|
|
inList = true;
|
|
}
|
|
|
|
// Add ordered list item
|
|
const itemContent = line.replace(/^\d+\. /, '');
|
|
listHtml += `<li>${itemContent}</li>`;
|
|
} else if (inList) {
|
|
// End the list
|
|
if (listHtml.includes('<ol>')) {
|
|
listHtml += '</ol>';
|
|
} else {
|
|
listHtml += '</ul>';
|
|
}
|
|
|
|
inList = false;
|
|
listHtml += line + '\n';
|
|
} else {
|
|
listHtml += line + '\n';
|
|
}
|
|
}
|
|
|
|
// Close list if still open at the end
|
|
if (inList) {
|
|
if (listHtml.includes('<ol>')) {
|
|
listHtml += '</ol>';
|
|
} else {
|
|
listHtml += '</ul>';
|
|
}
|
|
}
|
|
|
|
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];
|
|
});
|
|
}
|
|
}
|
|
};
|
|
} |