BREAKING CHANGE: All source files converted from JavaScript to TypeScript. - 12 .ts source files with strict types, full EditorOptions/Plugin/Token interfaces - 7 .ts test files, 610 total tests (27 new), 7 suites all passing - tsc --noEmit: 0 errors - rollup-plugin-typescript build: 5 artifacts (UMD/ESM/CJS/Min/DTS) - @babel/preset-typescript for jest - New tsconfig.json, updated babel/jest/rollup configs - Coverage: parser 99.5%, utils 95.7%, themes 96.2%, core 88.8%, plugins 89.5% - Removed types/ folder (types now inline in .ts + auto-generated .d.ts) - Desktop-only, no backward compatibility
103 lines
3.2 KiB
TypeScript
103 lines
3.2 KiB
TypeScript
/**
|
|
* 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, '"')
|
|
.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<T extends (...args: any[]) => void>(
|
|
func: T,
|
|
wait: number,
|
|
immediate: boolean = false,
|
|
): (...args: Parameters<T>) => void {
|
|
let timeout: ReturnType<typeof setTimeout> | null;
|
|
return function (this: any, ...args: Parameters<T>) {
|
|
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<T extends (...args: any[]) => void>(
|
|
func: T,
|
|
limit: number,
|
|
): (...args: Parameters<T>) => void {
|
|
let inThrottle = false;
|
|
return function (this: any, ...args: Parameters<T>) {
|
|
if (!inThrottle) {
|
|
func.apply(this, args);
|
|
inThrottle = true;
|
|
setTimeout(() => { inThrottle = false; }, limit);
|
|
}
|
|
};
|
|
}
|
|
|
|
/** Deep-merge two objects */
|
|
export const deepMerge = <T extends Record<string, unknown>>(target: T, source: Partial<T>): T => {
|
|
const output: Record<string, unknown> = { ...target };
|
|
for (const key of Object.keys(source as Record<string, unknown>)) {
|
|
const sv = (source as Record<string, unknown>)[key];
|
|
const tv = (target as Record<string, unknown>)[key];
|
|
if (sv instanceof Object && key in target && tv instanceof Object) {
|
|
output[key] = deepMerge(tv as Record<string, unknown>, sv as Record<string, unknown>);
|
|
} 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<void> => {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
};
|