|
| 1 | +// prettier-ignore |
| 2 | +import fonts from './unicode-fonts.json'; |
| 3 | + |
| 4 | +export type AllFontNames = keyof typeof fonts; |
| 5 | +export interface AllOptions { |
| 6 | + remove?: string |
| 7 | + append?: string |
| 8 | + reverse?: boolean |
| 9 | + clear?: boolean |
| 10 | +}; |
| 11 | + |
| 12 | +// list of font characters for checking if character is formatted |
| 13 | +const allCharacters = new Set(Object.values(fonts).join('')); |
| 14 | + |
| 15 | +// check if text is already formatted with a certain font |
| 16 | +function alreadyFormatted(text: string, font: AllFontNames) { |
| 17 | + const fontCharacters = new Set(fonts[font]); |
| 18 | + // flag as already formatted if all characters are in font or not in any other font |
| 19 | + return Array.from(text).every(char => fontCharacters.has(char) || !allCharacters.has(char)); |
| 20 | +} |
| 21 | + |
| 22 | +// check if text is already formatted with a certain font |
| 23 | +function alreadyAppended(text: string, append: string) { |
| 24 | + // check if at least half the characters are the append character |
| 25 | + return Array.from(text).filter(char => char === append).length >= text.length / 2; |
| 26 | +} |
| 27 | + |
| 28 | +// format text into selected font |
| 29 | +function formatText(text: string, font: AllFontNames | undefined, options?: AllOptions) { |
| 30 | + // set font to normal if already formatted with selected font |
| 31 | + if (font && fonts[font] && alreadyFormatted(text, font)) { |
| 32 | + font = 'normal'; |
| 33 | + } |
| 34 | + // remove and don't append if character is already appended |
| 35 | + if (options?.append) { |
| 36 | + options.remove = options.append; |
| 37 | + options.append = !alreadyAppended(text, options.append) ? options.append : ''; |
| 38 | + } |
| 39 | + // Array.from() splits the string by symbol and not by code points |
| 40 | + let newText = Array.from(text); |
| 41 | + // exchange font symbols |
| 42 | + if (font && fonts[font]) { |
| 43 | + const targetFont = Array.from(fonts[font]); |
| 44 | + const charLists = Object.values(fonts); |
| 45 | + // map characters to new font |
| 46 | + newText = newText.map((char) => { |
| 47 | + let index = -1; |
| 48 | + // find the index of the character in some font |
| 49 | + const found = charLists.some((charList) => { |
| 50 | + index = Array.from(charList).indexOf(char); |
| 51 | + return index > -1; |
| 52 | + }); |
| 53 | + // if found, replace with the corresponding character in the target font |
| 54 | + // if not found, keep the character the same |
| 55 | + return found ? targetFont[index] : char; |
| 56 | + }); |
| 57 | + } |
| 58 | + // reverse text if reverse option is set |
| 59 | + newText = options?.reverse ? newText.reverse() : newText; |
| 60 | + // remove appended symbol of specific type from the end |
| 61 | + newText = options?.remove |
| 62 | + ? newText.map(char => char.replace(new RegExp(`${options.remove}$`, 'u'), '')) |
| 63 | + : newText; |
| 64 | + // append symbol (underline, strikethrough, etc.) to end of each character if append is set |
| 65 | + newText = options?.append ? newText.map(char => char + options.append) : newText; |
| 66 | + // remove appended symbols (underline, strikethrough, etc.) if using eraser |
| 67 | + // \u035f = Underline, \u0333 = Double Underline, \u0335 = Short Strikethrough \u0336 = Strikethrough |
| 68 | + newText = options?.clear ? newText.map(char => char.replace(/\u035F|\u0333|\u0335|\u0336/gu, '')) : newText; |
| 69 | + // set textarea content and select text around the replacement |
| 70 | + return newText.join(''); |
| 71 | +} |
| 72 | + |
| 73 | +export function formatTextPart( |
| 74 | + text: string, |
| 75 | + selectionStart: number, selectionEnd: number, |
| 76 | + font: AllFontNames | undefined, |
| 77 | + options?: AllOptions) { |
| 78 | + const regexSpaces = /^(\s*)(.+?)(\s*)$/g; // NOSONAR |
| 79 | + const [_, spaceBefore, selection, spaceAfter] = regexSpaces.exec(text.substring(selectionStart, selectionEnd) || '') || []; |
| 80 | + |
| 81 | + const prefix = text.substring(0, selectionStart); |
| 82 | + const newSelection = formatText(selection, font, options); |
| 83 | + const suffix = text.substring(selectionEnd); |
| 84 | + return { |
| 85 | + text: `${prefix}${spaceBefore}${newSelection}${spaceAfter}${suffix}`, |
| 86 | + from: selectionStart, |
| 87 | + to: selectionStart + (spaceBefore?.length || 0) + newSelection.length + (spaceAfter?.length || 0), |
| 88 | + }; |
| 89 | +} |
| 90 | + |
| 91 | +// format selected text |
| 92 | +export function formatSelection(textArea: HTMLTextAreaElement, font: AllFontNames | undefined, options?: AllOptions) { |
| 93 | + const selectionStart = textArea.selectionStart; |
| 94 | + const selectionEnd = textArea.selectionEnd; |
| 95 | + const oldText = textArea.value; |
| 96 | + |
| 97 | + const { text: newText, from, to } = formatTextPart(oldText, selectionStart, selectionEnd, font, options); |
| 98 | + |
| 99 | + textArea.value = newText; |
| 100 | + textArea.setSelectionRange(from, to); |
| 101 | + |
| 102 | + textArea.focus(); |
| 103 | +} |
0 commit comments