This commit is contained in:
2026-07-27 00:56:18 +03:30
commit 1c75afe0fd
244 changed files with 27710 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
{
"name": "@renovamen/utils",
"version": "0.1.1",
"description": "Useful utils, zero dependencies.",
"homepage": "https://github.com/Renovamen/oh-my-cv/tree/main/packages/utils",
"bugs": {
"url": "https://github.com/Renovamen/oh-my-cv"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Renovamen/oh-my-cv.git",
"directory": "packages/utils"
},
"license": "MIT",
"author": "Renovamen <renovamenzxh@gmail.com>",
"files": [
"dist"
],
"exports": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"import": "./dist/index.mjs"
},
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"scripts": {
"build-fast:pkg": "tsup src/index.ts --format cjs,esm",
"build:pkg": "pnpm run build-fast:pkg --dts"
},
"publishConfig": {
"access": "public"
}
}
+40
View File
@@ -0,0 +1,40 @@
import { isObject } from "./is";
// Copied from https://github.com/meteorlxy/vscode-slugify
export const slugify = (str: string) =>
encodeURI(
str
.trim()
.toLowerCase()
.replace(/\s+/g, "-") // Replace whitespace with -
.replace(
/[\]\[\!\'\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~\`。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}]/g,
""
) // Remove known punctuators
.replace(/^\-+/, "") // Remove leading -
.replace(/\-+$/, "") // Remove trailing -
);
export const htmlEscape = (str: string) => {
const escapeMap: Record<string, string> = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
"'": "&#39;",
'"': "&quot;"
};
return str.replace(/[&<>'"]/g, (char) => escapeMap[char]);
};
export const copy = <T>(obj: T): T => {
if (isObject(obj)) return JSON.parse(JSON.stringify(obj));
throw new Error("Input must be a non-null object.");
};
export const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export const now = () => Date.now();
export const arrayify = <T>(value: T | T[]): T[] =>
Array.isArray(value) ? value : [value];
+89
View File
@@ -0,0 +1,89 @@
import type { Callback } from "./types";
export const fetchFile = async (url: string): Promise<string> => {
try {
const res = await fetch(url);
if (!res.ok) {
throw new Error(`Request error: ${res.status} ${res.statusText}`);
}
return await res.text();
} catch (error) {
return Promise.reject(error instanceof Error ? error : new Error(String(error)));
}
};
/**
* Open file dialog with ease. This hook differs from vueuse's useFileDialog in that it
* doesn't require Vue.
*
* @param accept File types to accept
* @returns
*/
export const useFileDialog = (accept?: string) => {
let callback: Callback<File> | null = null;
let input: HTMLInputElement | undefined;
if (document) {
input = document.createElement("input");
input.type = "file";
input.style.display = "none";
if (accept) input.accept = accept;
input.onchange = (event: Event) => {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (file && callback) callback(file);
};
}
const open = () => {
if (!input) return;
document.body.appendChild(input);
input.click();
document.body.removeChild(input);
};
const onChange = (cb: Callback<File>) => {
callback = cb;
};
return {
open,
onChange
};
};
/**
* Read file content as text.
*
* @param file File object
* @returns Promise containing file content as string
*/
export const readFile = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = () => reject(new Error("Failed to read file"));
reader.readAsText(file);
});
};
export const downloadFile = (filename: string, content: string) => {
const element = document.createElement("a");
element.href = "data:text/plain;charset=utf-8," + encodeURIComponent(content);
element.download = filename;
element.style.display = "none";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
};
+4
View File
@@ -0,0 +1,4 @@
export * from "./file";
export * from "./common";
export * from "./types";
export * from "./is";
+17
View File
@@ -0,0 +1,17 @@
export const isClient = typeof window !== "undefined" && typeof document !== "undefined";
export const isMac =
isClient && typeof navigator !== "undefined" && /Macintosh/.test(navigator.userAgent);
export const isExternal = (path: string) => {
const outboundRE = /^(https?:|mailto:|tel:)/;
return outboundRE.test(path);
};
export const isObject = (v: any) => toString.call(v) === "[object Object]";
export const isInteger = (v: any, { allowString = false } = {}): boolean => {
return typeof v === "number"
? Number.isInteger(v)
: allowString && typeof v === "string" && Number.isInteger(Number(v));
};
+15
View File
@@ -0,0 +1,15 @@
export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<T>;
export type PartialWithRequired<T, K extends keyof T> = Pick<T, K> & Partial<T>;
// https://stackoverflow.com/questions/55541275/typescript-check-for-the-any-type
// https://github.com/vueuse/vueuse/blob/main/packages/shared/utils/types.ts
export type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N;
export type IsAny<T> = IfAny<T, true, false>;
export type Callback<T> =
IsAny<T> extends true
? (param: any) => void
: [T] extends [void]
? () => void
: (param: T) => void;
+4
View File
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.base.json",
"include": ["./src"]
}