import { Anchor, Text, useComputedColorScheme, useMantineTheme } from "@mantine/core"; import React from "react"; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import dark from 'react-syntax-highlighter/dist/esm/styles/prism/one-dark'; import light from 'react-syntax-highlighter/dist/esm/styles/prism/one-light'; import { UserMention } from "../UserMention/UserMention"; import { Emoji } from "../Emoji/Emoji"; import { GroupInviteMessage } from "../GroupInviteMessage/GroupInviteMessage"; import { ALLOWED_DOMAINS_ZONES } from "@/app/constants"; interface TextParserProps { text: string; /** * If true, the parsed entities will be rendered without hydration (static rendering). */ noHydrate?: boolean; /** * If the text (excluding emojis) is smaller than this value, render emojis in oversize (40px). */ oversizeIfTextSmallerThan?: number; /** * Limits the number of parsed entities (like links, mentions, emojis, etc.) in the text. * If the limit is reached, the remaining text will be rendered as plain text. */ performanceEntityLimit?: number; /** * Flags to enable other effects */ __reserved_1?: boolean; __reserved_2?: boolean; __reserved_3?: boolean; } interface FormatRule { pattern: RegExp[]; render: (match: string) => React.ReactNode; flush: (match: string) => React.ReactNode; } export function TextParser(props: TextParserProps) { const computedTheme = useComputedColorScheme(); const theme = useMantineTheme(); let entityCount = 0; const formatRules : FormatRule[] = [ { pattern: [ /(https?:\/\/[^\s]+)/g, /\b(?:https?:\/\/)?(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(?:\/[^\s]*)?/g ], render: (match: string) => { let domainZone = match.split('.').pop() || ''; domainZone = domainZone.split('/')[0]; if(!ALLOWED_DOMAINS_ZONES.includes(domainZone)) { return <>{match}; } return {match}; }, flush: (match: string) => { return <>{match}; } }, { pattern: [/\*\*(.+?)\*\*/], render: (match: string) => { const boldText = match.replace(/^\*\*(.+)\*\*$/, '$1'); return {boldText}; }, flush: (match: string) => { const boldText = match.replace(/^\*\*(.+)\*\*$/, '$1'); return <>{boldText}; } }, { // language```code``` pattern: [/([a-zA-Z0-9]+)```([\s\S]*?)```/], render: (match: string) => { const langMatch = match.match(/^([a-zA-Z0-9]+)```([\s\S]*?)```$/); const language = langMatch ? langMatch[1] : "plaintext"; const codeContent = langMatch ? langMatch[2] : match; return ( {codeContent.trim()} ); }, flush: (match: string) => { const langMatch = match.match(/^([a-zA-Z0-9]+)```([\s\S]*?)```$/); const codeContent = langMatch ? langMatch[2] : match; return <>{codeContent}; } }, { // @username pattern: [/@([a-zA-Z0-9_]+)/], render: (match: string) => { return ; }, flush: (match: string) => { return <>{match}; } }, { // :emoji_code: pattern: [/:emoji_([a-zA-Z0-9_-]+):/], render: (match: string) => { const emojiCode = match.slice(1, -1); let textWithoutEmojis = props.text.replace(/:emoji_[a-zA-Z0-9_-]+:/g, ''); if(textWithoutEmojis.length <= (props.oversizeIfTextSmallerThan ?? 0)) { return ; } return ; }, flush: (match: string) => { const emojiCode = match.slice(1, -1); return ; } }, { // $a=Attachment text pattern: [/^\$a=(.+)$/], render: (match: string) => { const attachmentText = match.replace(/^\$a=(.+)$/, '$1'); return <>{attachmentText}; }, flush: (match: string) => { const attachmentText = match.replace(/^\$a=(.+)$/, '$1'); return {attachmentText}; } }, { //#group:stringbase64 pattern: [/^#group:([A-Za-z0-9+/=:]+)$/], render: (match: string) => { const groupString = match.replace(/^#group:([A-Za-z0-9+/=]+)$/, '$1'); return ; }, flush: () => { return Group Invite Code; } } ]; function parseText(text: string): React.ReactNode[] { let result: React.ReactNode[] = []; let remainingText = text; let index = 0; while (remainingText.length > 0) { let earliestMatch: {start: number, end: number, rule: FormatRule, match: string} | null = null; for (const rule of formatRules) { for (const pattern of rule.pattern) { pattern.lastIndex = 0; // Reset regex state for global patterns const match = pattern.exec(remainingText); if (match && (earliestMatch === null || match.index < earliestMatch.start)) { earliestMatch = { start: match.index, end: match.index + match[0].length, rule, match: match[0] }; } } } if (earliestMatch) { // Performance limit check if (props.performanceEntityLimit !== undefined && entityCount >= props.performanceEntityLimit) { result.push( {remainingText} ); break; } entityCount += 1; if (earliestMatch.start > 0) { result.push( {remainingText.slice(0, earliestMatch.start)} ); } result.push( {props.noHydrate ? earliestMatch.rule.flush(earliestMatch.match) : earliestMatch.rule.render(earliestMatch.match)} ); remainingText = remainingText.slice(earliestMatch.end); } else { result.push( {remainingText} ); break; } } return result; } return <>{parseText(props.text)}; }