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)}>;
}