import type { ReactNode, PropsWithChildren, } from "react"; import { Children, isValidElement, cloneElement, Fragment, } from "react"; type TCallableComponent = (props: unknown) => ReactNode; const isCallableComponent = (type: unknown): type is TCallableComponent => { return typeof type === "function" && !type.prototype?.render && type !== Fragment; }; const SHORT_WORDS = [ "в", "во", "без", "до", "из", "к", "ко", "на", "по", "о", "от", "перед", "при", "через", "с", "у", "над", "за", "и", "да", "ни", "но", "или", "ли", "же", "бы", "б", "ль", "ли", "разве", "ведь", "вот", "вон", "ка", "то", "не", "ни", "а", "что", "как", "это", "этот", "такой", "такая", "такое", "такие", "все", "всё", "весь", "вся", "всю", "всех", "всем", "всеми", "тот", "та", "те", "тех", "тем", "теми", "сей", "сего", "сему", "сим", "се", "оный", "оного", "оному", "оным", "оном", "она", "оне", "они", "кои", "кая", "a", "an", "the", "and", "or", "but", "nor", "for", "so", "yet", "as", "if", "than", "that", "till", "until", "when", "where", "whether", "while", "at", "by", "in", "of", "on", "to", "with", "from", "into", "upon", "among", "about", "vs.", "v.", "versus", "etc.", "e.g.", "i.e.", "cf.", ]; /** * Получает текст с неразрывными пробелами * @param text{String} * @returns {String} */ const getStrWithSpaces = (text: string): string => { const result = text .replace(/\s+/g, " ") .replaceAll(" - ", "\u00A0—\u00A0") .replaceAll(" -- ", "\u00A0—\u00A0"); const tokens = result.split(/([\s\-]+)/); for (let i = 0; i < tokens.length - 2; i++) { if (tokens[i] === " ") { continue; } const currentWord = tokens[i].toLowerCase().replace(/[^a-zа-яё]/g, ""); const nextWord = tokens[i + 2].toLowerCase().replace(/[^a-zа-яё]/g, ""); // Если текущее или следующее слово - короткое, заменяем пробел на неразрывный if (SHORT_WORDS.includes(currentWord) || SHORT_WORDS.includes(nextWord)) { if (tokens[i + 1] === " ") { tokens[i + 1] = "\u00A0"; } else if (/^\s+$/.test(tokens[i + 1])) { tokens[i + 1] = tokens[i + 1].replace(/\s/g, "\u00A0"); } } } return tokens.join(""); }; /** * Получает дочерние компоненты с исправленным текстом * @param children{ReactNode} * @returns {ReactElement} */ const getFormattedChildren = (children: ReactNode): ReactNode => { return Children.map(children, (child) => { switch (true) { // Обработка строк и чисел case typeof child === "string" || typeof child === "number": { return getStrWithSpaces(child.toString()); } // Пропускаем null, undefined, boolean case !isValidElement(child): { return child; } // Обработка Fragment (чтобы не ломать структуру) case (isValidElement(child) && child.type === Fragment): { const props = (child?.props ?? {}) as PropsWithChildren>; return cloneElement(child, props, getFormattedChildren(props.children)); } // Обработка обычных элементов и компонентов case (isValidElement>>(child)): { const { children: childChildren, ...restProps } = child.props; // Если это компонент (функция), вызываем его с props и форматируем children if (isCallableComponent(child.type)) { try { const renderedChild = child.type(child.props); return getFormattedChildren(renderedChild); } catch (e) { console.warn("[Text] Failed to render component:", e); return child; } } // Если это обычный элемент (div, span и т.д.), форматируем его children return cloneElement(child, restProps, getFormattedChildren(childChildren)); } default: return child; } }); }; export { getFormattedChildren, getStrWithSpaces, };