/** * MetonaEditor Utils — utility functions * @module utils * @version 0.2.0 */ /** Generate a unique ID */ export const generateId = (): string => { return 'me-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8); }; /** HTML-escape a string */ export const escapeHTML = (s: unknown): string => { const str = String(s == null ? '' : s); if (typeof document === 'undefined') { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } const div = document.createElement('div'); div.textContent = str; return div.innerHTML; }; /** Detect dark mode preference */ export const prefersDark = (): boolean => { if (typeof window === 'undefined' || !window.matchMedia) return false; return window.matchMedia('(prefers-color-scheme: dark)').matches; }; /** Debounce a function */ export function debounce void>( func: T, wait: number, immediate: boolean = false, ): (...args: Parameters) => void { let timeout: ReturnType | null; return function (this: any, ...args: Parameters) { const context = this; const later = () => { timeout = null; if (!immediate) func.apply(context, args); }; const callNow = immediate && !timeout; if (timeout) clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; } /** Throttle a function */ export function throttle void>( func: T, limit: number, ): (...args: Parameters) => void { let inThrottle = false; return function (this: any, ...args: Parameters) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => { inThrottle = false; }, limit); } }; } /** Deep-merge two objects */ export const deepMerge = >(target: T, source: Partial): T => { const output: Record = { ...target }; for (const key of Object.keys(source as Record)) { const sv = (source as Record)[key]; const tv = (target as Record)[key]; if (sv instanceof Object && key in target && tv instanceof Object) { output[key] = deepMerge(tv as Record, sv as Record); } else { output[key] = source[key]; } } return output as T; }; /** Check if running in browser */ export const isBrowser = (): boolean => { return typeof window !== 'undefined' && typeof document !== 'undefined'; }; /** Format file size to human-readable string */ export const formatFileSize = (bytes: number, decimals: number = 2): string => { if (bytes === 0) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; }; /** Sleep for ms milliseconds */ export const sleep = (ms: number): Promise => { return new Promise((resolve) => setTimeout(resolve, ms)); };