import DOMPurify from "isomorphic-dompurify"; /** * @description Adds space between camelCase words * @param {string} str - String to add spaces to * @returns {string} String with spaces between camelCase words * @example * addSpaceIfCamelCase("camelCase") // returns "camel Case" * addSpaceIfCamelCase("thisIsATest") // returns "this Is A Test" */ export const addSpaceIfCamelCase = (str: string) => { if (str === undefined || str === null) return ""; if (typeof str !== "string") str = `${str}`; return str.replace(/([a-z])([A-Z])/g, "$1 $2"); }; /** * @description Replaces underscores with spaces in snake_case strings * @param {string} str - String to replace underscores in * @returns {string} String with underscores replaced by spaces * @example * replaceUnderscoreIfSnakeCase("snake_case") // returns "snake case" */ export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " "); /** * @description Truncates text to specified length and adds ellipsis * @param {string} str - String to truncate * @param {number} length - Maximum length before truncation * @returns {string} Truncated string with ellipsis if needed * @example * truncateText("This is a long text", 7) // returns "This is..." */ export const truncateText = (str: string, length: number) => { if (!str || str === "") return ""; return str.length > length ? `${str.substring(0, length)}...` : str; }; /** * @description Creates a similar string by randomly shuffling characters * @param {string} str - String to shuffle * @returns {string} Shuffled string with same characters * @example * createSimilarString("hello") // might return "olleh" or "lehol" */ export const createSimilarString = (str: string) => { const shuffled = str .split("") .sort(() => Math.random() - 0.5) .join(""); return shuffled; }; /** * @description Copies full URL (origin + path) to clipboard * @param {string} path - URL path to copy * @returns {Promise} Promise that resolves when copying is complete * @example * await copyUrlToClipboard("issues/123") // copies "https://example.com/issues/123" */ export const copyUrlToClipboard = async (path: string) => { // get origin or default to empty string if not in browser const originUrl = typeof window !== "undefined" ? window.location.origin : ""; // create URL object and ensure proper path formatting const url = new URL(path, originUrl); await copyTextToClipboard(url.toString()); }; /** * @description Gets first character of first word or first characters of first two words * @param {string} str - Input string * @returns {string} First character(s) * @example * getFirstCharacters("John") // returns "J" * getFirstCharacters("John Doe") // returns "JD" */ export const getFirstCharacters = (str: string) => { const words = str.trim().split(" "); if (words.length === 1) { return words[0].charAt(0); } else { return words[0].charAt(0) + words[1].charAt(0); } }; /** * @description Formats number count, showing "99+" for numbers over 99 * @param {number} number - Number to format * @returns {string} Formatted number string * @example * getNumberCount(50) // returns "50" * getNumberCount(100) // returns "99+" */ export const getNumberCount = (number: number): string => { if (number > 99) { return "99+"; } return number.toString(); }; /** * @description Converts object to URL query parameters string * @param {Object} obj - Object to convert * @returns {string} URL query parameters string * @example * objToQueryParams({ page: 1, search: "test" }) // returns "page=1&search=test" * objToQueryParams({ a: null, b: "test" }) // returns "b=test" */ export const objToQueryParams = (obj: any) => { const params = new URLSearchParams(); if (!obj) return params.toString(); for (const [key, value] of Object.entries(obj)) { if (value !== undefined && value !== null) params.append(key, value as string); } return params.toString(); }; /** * @description: This function will capitalize the first letter of a string * @param str String * @returns String */ export const capitalizeFirstLetter = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); /** * @description : This function will remove all the HTML tags from the string * @param {string} htmlString * @return {string} * @example : * const html = "

Some text

"; const text = stripHTML(html); console.log(text); // Some text */ export const sanitizeHTML = (htmlString: string) => { const sanitizedText = DOMPurify.sanitize(htmlString, { ALLOWED_TAGS: [] }); // sanitize the string to remove all HTML tags return sanitizedText.trim(); // trim the string to remove leading and trailing whitespaces }; /** * @description: This function will remove all the HTML tags from the string and truncate the string to the specified length * @param {string} html * @param {number} length * @return {string} * @example: * const html = "

Some text

"; * const text = stripAndTruncateHTML(html); * console.log(text); // Some text */ export const stripAndTruncateHTML = (html: string, length: number = 55) => truncateText(sanitizeHTML(html), length); /** * @returns {boolean} true if email is valid, false otherwise * @description Returns true if email is valid, false otherwise * @param {string} email string to check if it is a valid email * @example checkEmailValidity("hello world") => false * @example checkEmailValidity("example@plane.so") => true */ export const checkEmailValidity = (email: string): boolean => { if (!email) return false; const isEmailValid = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( email ); return isEmailValid; }; export const isEmptyHtmlString = (htmlString: string, allowedHTMLTags: string[] = []) => { // Remove HTML tags using DOMPurify const cleanText = DOMPurify.sanitize(htmlString, { ALLOWED_TAGS: allowedHTMLTags }); // Trim the string and check if it's empty return cleanText.trim() === ""; }; /** * @description this function returns whether a comment is empty or not by checking for the following conditions- * 1. If comment is undefined * 2. If comment is an empty string * 3. If comment is "

" * @param {string | undefined} comment * @returns {boolean} */ export const isCommentEmpty = (comment: string | undefined): boolean => { // return true if comment is undefined if (!comment) return true; return ( comment?.trim() === "" || comment === "

" || isEmptyHtmlString(comment ?? "", ["img", "mention-component", "image-component"]) ); }; /** * @description * This function test whether a URL is valid or not. * * It accepts URLs with or without the protocol. * @param {string} url * @returns {boolean} * @example * checkURLValidity("https://example.com") => true * checkURLValidity("example.com") => true * checkURLValidity("example") => false */ export const checkURLValidity = (url: string): boolean => { if (!url) return false; // regex to support complex query parameters and fragments const urlPattern = /^(https?:\/\/)?((([a-z\d-]+\.)*[a-z\d-]+\.[a-z]{2,6})|(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))(:\d+)?(\/[\w.-]*)*(\?[^#\s]*)?(#[\w-]*)?$/i; return urlPattern.test(url); }; /** * Combines array elements with a separator and adds a conjunction before the last element * @param array Array of strings to combine * @param separator Separator to use between elements (default: ", ") * @param conjunction Conjunction to use before last element (default: "and") * @returns Combined string with conjunction before the last element */ export const joinWithConjunction = (array: string[], separator: string = ", ", conjunction: string = "and"): string => { if (!array || array.length === 0) return ""; if (array.length === 1) return array[0]; if (array.length === 2) return `${array[0]} ${conjunction} ${array[1]}`; const lastElement = array[array.length - 1]; const elementsExceptLast = array.slice(0, -1); return `${elementsExceptLast.join(separator)}${separator}${conjunction} ${lastElement}`; }; /** * @description Ensures a URL has a protocol * @param {string} url * @returns {string} * @example * ensureUrlHasProtocol("example.com") => "http://example.com" */ export const ensureUrlHasProtocol = (url: string): string => (url.startsWith("http") ? url : `http://${url}`); /** * @returns {boolean} true if searchQuery is substring of text in the same order, false otherwise * @description Returns true if searchQuery is substring of text in the same order, false otherwise * @param {string} text string to compare from * @param {string} searchQuery * @example substringMatch("hello world", "hlo") => true * @example substringMatch("hello world", "hoe") => false */ export const substringMatch = (text: string, searchQuery: string): boolean => { try { let searchIndex = 0; for (let i = 0; i < text.length; i++) { if (text[i].toLowerCase() === searchQuery[searchIndex]?.toLowerCase()) searchIndex++; // All characters of searchQuery found in order if (searchIndex === searchQuery.length) return true; } // Not all characters of searchQuery found in order return false; } catch (error) { return false; } }; /** * @description Copies text to clipboard * @param {string} text - Text to copy * @returns {Promise} Promise that resolves when copying is complete * @example * await copyTextToClipboard("Hello, World!") // copies "Hello, World!" to clipboard */ const fallbackCopyTextToClipboard = (text: string) => { const textArea = document.createElement("textarea"); textArea.value = text; // Avoid scrolling to bottom textArea.style.top = "0"; textArea.style.left = "0"; textArea.style.position = "fixed"; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { // FIXME: Even though we are using this as a fallback, execCommand is deprecated 👎. We should find a better way to do this. // https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand document.execCommand("copy"); } catch (err) { // catch fallback error } document.body.removeChild(textArea); }; /** * @description Copies text to clipboard * @param {string} text - Text to copy * @returns {Promise} Promise that resolves when copying is complete * @example * await copyTextToClipboard("Hello, World!") // copies "Hello, World!" to clipboard */ export const copyTextToClipboard = async (text: string): Promise => { if (!navigator.clipboard) { fallbackCopyTextToClipboard(text); return; } await navigator.clipboard.writeText(text); }; /** * @description Joins URL path segments properly, removing duplicate slashes * @param {...string} segments - URL path segments to join * @returns {string} Properly joined URL path * @example * joinUrlPath("/workspace", "/projects") => "/workspace/projects" * joinUrlPath("/workspace", "projects") => "/workspace/projects" * joinUrlPath("workspace", "projects") => "workspace/projects" * joinUrlPath("/workspace/", "/projects/") => "/workspace/projects/" */ export const joinUrlPath = (...segments: string[]): string => { if (segments.length === 0) return ""; // Filter out empty segments const validSegments = segments.filter((segment) => segment !== ""); if (validSegments.length === 0) return ""; // Join segments and normalize slashes const joined = validSegments .map((segment, index) => { // Remove leading slashes from all segments except the first if (index > 0) { segment = segment.replace(/^\/+/, ""); } // Remove trailing slashes from all segments except the last if (index < validSegments.length - 1) { segment = segment.replace(/\/+$/, ""); } return segment; }) .join("/"); // Clean up any duplicate slashes that might have been created return joined.replace(/\/+/g, "/"); };