Files
MetonaEditor/src/utils.ts
T
thzxx e83fc211dc feat: v0.2.0 — TypeScript full rewrite, 95%+ core coverage
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
2026-07-24 22:28:38 +08:00

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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
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));
};