diff --git a/src/api/utils/RoomChatFormatter.ts b/src/api/utils/RoomChatFormatter.ts index 949de56..4a35a6e 100644 --- a/src/api/utils/RoomChatFormatter.ts +++ b/src/api/utils/RoomChatFormatter.ts @@ -80,11 +80,97 @@ const applyWiredTextMarkup = (content: string) => return result; }; +const FONT_NAMED_COLORS = new Set([ + 'red', 'green', 'blue', 'yellow', 'white', 'black', + 'orange', 'cyan', 'brown', 'purple', 'pink', 'magenta', + 'violet', 'gray', 'grey', 'lime', 'teal', 'gold', + 'silver', 'navy', 'maroon', 'olive', 'indigo' +]); + +export const sanitizeFontColor = (raw: string | null | undefined): string | null => +{ + if(!raw) return null; + if(raw.length > 20) return null; + + const value = raw.trim().toLowerCase(); + + if(/^#([0-9a-f]{3}|[0-9a-f]{6})$/.test(value)) return value; + if(FONT_NAMED_COLORS.has(value)) return value; + + return null; +}; + +export type FontSegment = { color: string | null; text: string }; + +const FONT_COLOR_ATTR = /color\s*=\s*(?:"([^"]{1,32})"|'([^']{1,32})'|([^\s"'>]{1,32}))/i; + +export const parseFontSegments = (input: string): FontSegment[] => +{ + if(!input) return []; + + const pattern = /]{0,200}?)>([\s\S]{0,200}?)<\/font>/gi; + const segments: FontSegment[] = []; + + let lastIndex = 0; + let match: RegExpExecArray | null; + + while((match = pattern.exec(input)) !== null) + { + if(match.index > lastIndex) + { + segments.push({ color: null, text: input.slice(lastIndex, match.index) }); + } + + const colorMatch = FONT_COLOR_ATTR.exec(match[1] || ''); + const rawColor = colorMatch ? (colorMatch[1] || colorMatch[2] || colorMatch[3]) : null; + const color = sanitizeFontColor(rawColor); + + segments.push({ color, text: match[2] }); + lastIndex = pattern.lastIndex; + } + + if(lastIndex < input.length) + { + segments.push({ color: null, text: input.slice(lastIndex) }); + } + + return segments; +}; + +const applyFontMarkup = (content: string) => +{ + const fontPattern = /<font\b([^&]{0,200}?)>([\s\S]{0,4000}?)<\/font>/gi; + const colorAttr = /color\s*=\s*(?:"([^"]{1,32})"|'([^']{1,32})'|([^\s"'>]{1,32}))/i; + + let previous = ''; + let next = content; + let guard = 0; + + while((previous !== next) && (guard < 20)) + { + previous = next; + next = next.replace(fontPattern, (_match, attrs: string, inner: string) => + { + const colorMatch = colorAttr.exec(attrs || ''); + const rawColor = colorMatch ? (colorMatch[1] || colorMatch[2] || colorMatch[3]) : null; + const color = sanitizeFontColor(rawColor); + + if(!color) return inner; + + return `${ inner }`; + }); + guard++; + } + + return next; +}; + export const RoomChatFormatter = (content: string) => { let result = ''; content = encodeHTML(content); + content = applyFontMarkup(content); content = applyWiredTextMarkup(content); //content = (joypixels.shortnameToUnicode(content) as string) diff --git a/src/common/UserIdentityView.tsx b/src/common/UserIdentityView.tsx index fac5e9a..019eb21 100644 --- a/src/common/UserIdentityView.tsx +++ b/src/common/UserIdentityView.tsx @@ -1,6 +1,23 @@ -import { FC, useMemo } from 'react'; +import { FC, Fragment, ReactNode, useMemo } from 'react'; import { GetNickIconUrl } from '../assets/images/user_custom/nick_icons'; -import { PREFIX_EFFECT_KEYFRAMES, getPrefixEffectStyle, getPrefixFontStyle, parsePrefixColors } from '../api'; +import { PREFIX_EFFECT_KEYFRAMES, getPrefixEffectStyle, getPrefixFontStyle, parseFontSegments, parsePrefixColors } from '../api'; + +const renderInlineFontMarkup = (text: string): ReactNode => +{ + if(!text) return text; + if(text.indexOf(' + { + if(segment.color) return { segment.text }; + + return { segment.text }; + }); +}; interface UserIdentityViewProps { @@ -87,7 +104,7 @@ export const UserIdentityView: FC = ({ ); case 'name': - return { username }{ showColon ? ':' : '' }{ showColon ? ' ' : '' }; + return { renderInlineFontMarkup(username) }{ showColon ? ':' : '' }{ showColon ? ' ' : '' }; default: return null; }