mime change
This commit is contained in:
parent
d62b6d2a34
commit
f4990623da
@ -40,6 +40,8 @@ import {
|
||||
} from '@/lib/infomaniak-mime-decoder';
|
||||
import DOMPurify from 'isomorphic-dompurify';
|
||||
import ComposeEmail from '@/components/ComposeEmail';
|
||||
import { decodeEmail } from '@/lib/mail-parser-wrapper';
|
||||
import { Attachment as MailParserAttachment } from 'mailparser';
|
||||
|
||||
export interface Account {
|
||||
id: number;
|
||||
@ -108,188 +110,67 @@ function splitEmailHeadersAndBody(emailBody: string): { headers: string; body: s
|
||||
};
|
||||
}
|
||||
|
||||
function renderEmailContent(email: Email) {
|
||||
async function renderEmailContent(email: Email) {
|
||||
if (!email.body) {
|
||||
console.warn('No email body provided');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Split email into headers and body
|
||||
const [headersPart, ...bodyParts] = email.body.split('\r\n\r\n');
|
||||
if (!headersPart || bodyParts.length === 0) {
|
||||
throw new Error('Invalid email format: missing headers or body');
|
||||
const decoded = await decodeEmail(email.body);
|
||||
|
||||
// Prefer HTML content if available
|
||||
if (decoded.html) {
|
||||
return (
|
||||
<div className="email-content" dir="ltr">
|
||||
<div className="prose max-w-none" dir="ltr" dangerouslySetInnerHTML={{ __html: cleanHtml(decoded.html) }} />
|
||||
{decoded.attachments.length > 0 && renderAttachments(decoded.attachments)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const body = bodyParts.join('\r\n\r\n');
|
||||
|
||||
// Parse headers using Infomaniak MIME decoder
|
||||
const headerInfo = parseEmailHeaders(headersPart);
|
||||
const boundary = extractBoundary(headersPart);
|
||||
|
||||
// If it's a multipart email
|
||||
if (boundary) {
|
||||
try {
|
||||
const parts = body.split(`--${boundary}`);
|
||||
let htmlContent = '';
|
||||
let textContent = '';
|
||||
let attachments: { filename: string; content: string }[] = [];
|
||||
|
||||
for (const part of parts) {
|
||||
if (!part.trim()) continue;
|
||||
|
||||
const [partHeaders, ...partBodyParts] = part.split('\r\n\r\n');
|
||||
if (!partHeaders || partBodyParts.length === 0) continue;
|
||||
|
||||
const partBody = partBodyParts.join('\r\n\r\n');
|
||||
const contentType = extractHeader(partHeaders, 'Content-Type').toLowerCase();
|
||||
const encoding = extractHeader(partHeaders, 'Content-Transfer-Encoding').toLowerCase();
|
||||
const charset = extractHeader(partHeaders, 'charset') || 'utf-8';
|
||||
|
||||
try {
|
||||
let decodedContent = '';
|
||||
if (encoding === 'base64') {
|
||||
decodedContent = decodeBase64(partBody, charset);
|
||||
} else if (encoding === 'quoted-printable') {
|
||||
decodedContent = decodeQuotedPrintable(partBody, charset);
|
||||
} else {
|
||||
decodedContent = convertCharset(partBody, charset);
|
||||
}
|
||||
|
||||
if (contentType.includes('text/html')) {
|
||||
// For HTML content, we want to preserve the HTML structure
|
||||
// Only clean up problematic elements while keeping the formatting
|
||||
htmlContent = decodedContent
|
||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
||||
.replace(/<meta[^>]*>/gi, '')
|
||||
.replace(/<link[^>]*>/gi, '')
|
||||
.replace(/<base[^>]*>/gi, '')
|
||||
.replace(/<title[^>]*>[\s\S]*?<\/title>/gi, '')
|
||||
.replace(/<head[^>]*>[\s\S]*?<\/head>/gi, '')
|
||||
.replace(/<body[^>]*>/gi, '')
|
||||
.replace(/<\/body>/gi, '')
|
||||
.replace(/<html[^>]*>/gi, '')
|
||||
.replace(/<\/html>/gi, '');
|
||||
} else if (contentType.includes('text/plain')) {
|
||||
textContent = decodedContent;
|
||||
} else if (contentType.includes('attachment') || extractHeader(partHeaders, 'Content-Disposition').includes('attachment')) {
|
||||
attachments.push({
|
||||
filename: extractFilename(partHeaders) || 'unnamed_attachment',
|
||||
content: decodedContent
|
||||
});
|
||||
}
|
||||
} catch (partError) {
|
||||
console.error('Error processing email part:', partError);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer HTML content if available
|
||||
if (htmlContent) {
|
||||
return (
|
||||
<div className="email-content" dir="ltr">
|
||||
<div className="prose max-w-none" dir="ltr" dangerouslySetInnerHTML={{ __html: htmlContent }} />
|
||||
{attachments.length > 0 && renderAttachments(attachments)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Fall back to text content
|
||||
if (textContent) {
|
||||
return (
|
||||
<div className="email-content" dir="ltr">
|
||||
<div className="whitespace-pre-wrap font-sans text-base leading-relaxed" dir="ltr">
|
||||
{textContent.split('\n').map((line: string, i: number) => (
|
||||
<p key={i} className="mb-2">{line}</p>
|
||||
))}
|
||||
</div>
|
||||
{attachments.length > 0 && renderAttachments(attachments)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} catch (multipartError) {
|
||||
console.error('Error processing multipart email:', multipartError);
|
||||
throw new Error('Failed to process multipart email');
|
||||
}
|
||||
}
|
||||
|
||||
// If it's a simple email, try to detect content type and decode
|
||||
const contentType = extractHeader(headersPart, 'Content-Type').toLowerCase();
|
||||
const encoding = extractHeader(headersPart, 'Content-Transfer-Encoding').toLowerCase();
|
||||
const charset = extractHeader(headersPart, 'charset') || 'utf-8';
|
||||
|
||||
try {
|
||||
let decodedBody = '';
|
||||
if (encoding === 'base64') {
|
||||
decodedBody = decodeBase64(body, charset);
|
||||
} else if (encoding === 'quoted-printable') {
|
||||
decodedBody = decodeQuotedPrintable(body, charset);
|
||||
} else {
|
||||
decodedBody = convertCharset(body, charset);
|
||||
}
|
||||
|
||||
if (contentType.includes('text/html')) {
|
||||
// For HTML content, preserve the HTML structure
|
||||
const cleanedHtml = decodedBody
|
||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
||||
.replace(/<meta[^>]*>/gi, '')
|
||||
.replace(/<link[^>]*>/gi, '')
|
||||
.replace(/<base[^>]*>/gi, '')
|
||||
.replace(/<title[^>]*>[\s\S]*?<\/title>/gi, '')
|
||||
.replace(/<head[^>]*>[\s\S]*?<\/head>/gi, '')
|
||||
.replace(/<body[^>]*>/gi, '')
|
||||
.replace(/<\/body>/gi, '')
|
||||
.replace(/<html[^>]*>/gi, '')
|
||||
.replace(/<\/html>/gi, '');
|
||||
|
||||
return (
|
||||
<div className="email-content" dir="ltr">
|
||||
<div className="prose max-w-none" dir="ltr" dangerouslySetInnerHTML={{ __html: cleanedHtml }} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="email-content" dir="ltr">
|
||||
// Fall back to text content
|
||||
if (decoded.text) {
|
||||
return (
|
||||
<div className="email-content" dir="ltr">
|
||||
<div className="whitespace-pre-wrap font-sans text-base leading-relaxed" dir="ltr">
|
||||
{decodedBody.split('\n').map((line: string, i: number) => (
|
||||
{decoded.text.split('\n').map((line: string, i: number) => (
|
||||
<p key={i} className="mb-2">{line}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} catch (decodeError) {
|
||||
console.error('Error decoding email body:', decodeError);
|
||||
throw new Error('Failed to decode email body');
|
||||
{decoded.attachments.length > 0 && renderAttachments(decoded.attachments)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error rendering email content:', error);
|
||||
return (
|
||||
<div className="email-content">
|
||||
<div className="text-red-500 mb-4">Error displaying email content: {error instanceof Error ? error.message : 'Unknown error'}</div>
|
||||
<pre className="whitespace-pre-wrap text-sm bg-gray-100 p-4 rounded">
|
||||
{email.body}
|
||||
</pre>
|
||||
<div className="email-content text-red-500">
|
||||
Error rendering email content
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to render attachments
|
||||
function renderAttachments(attachments: { filename: string; content: string }[]) {
|
||||
function renderAttachments(attachments: MailParserAttachment[]) {
|
||||
if (!attachments.length) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<h3 className="text-sm font-medium mb-2">Attachments:</h3>
|
||||
<ul className="space-y-2">
|
||||
<div className="mt-4 border-t border-gray-200 pt-4">
|
||||
<h3 className="text-sm font-medium text-gray-900 mb-2">Attachments</h3>
|
||||
<div className="space-y-2">
|
||||
{attachments.map((attachment, index) => (
|
||||
<li key={index} className="flex items-center gap-2">
|
||||
<Paperclip className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm">{attachment.filename}</span>
|
||||
</li>
|
||||
<div key={index} className="flex items-center gap-2 p-2 bg-gray-50 rounded">
|
||||
<Paperclip className="h-4 w-4 text-gray-400" />
|
||||
<span className="text-sm text-gray-600">{attachment.filename || 'unnamed_attachment'}</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
{attachment.size ? `(${Math.round(attachment.size / 1024)} KB)` : ''}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -328,124 +209,43 @@ const initialSidebarItems = [
|
||||
}
|
||||
];
|
||||
|
||||
function getReplyBody(email: Email, type: 'reply' | 'reply-all' | 'forward' = 'reply') {
|
||||
async function getReplyBody(email: Email, type: 'reply' | 'reply-all' | 'forward' = 'reply') {
|
||||
if (!email.body) return '';
|
||||
|
||||
let content = '';
|
||||
let headers = '';
|
||||
let body = '';
|
||||
|
||||
// Split headers and body
|
||||
const parts = email.body.split('\r\n\r\n');
|
||||
if (parts.length > 1) {
|
||||
headers = parts[0];
|
||||
body = parts.slice(1).join('\r\n\r\n');
|
||||
} else {
|
||||
body = email.body;
|
||||
}
|
||||
|
||||
// Parse headers
|
||||
const headerInfo = parseEmailHeaders(headers);
|
||||
const boundary = extractBoundary(headers);
|
||||
|
||||
// Handle multipart emails
|
||||
if (boundary) {
|
||||
const parts = body.split(`--${boundary}`);
|
||||
for (const part of parts) {
|
||||
if (!part.trim()) continue;
|
||||
|
||||
const [partHeaders, ...partBodyParts] = part.split('\r\n\r\n');
|
||||
if (!partHeaders || partBodyParts.length === 0) continue;
|
||||
|
||||
const partBody = partBodyParts.join('\r\n\r\n');
|
||||
const partHeaderInfo = parseEmailHeaders(partHeaders);
|
||||
|
||||
try {
|
||||
let decodedContent = '';
|
||||
if (partHeaderInfo.encoding === 'base64') {
|
||||
decodedContent = decodeBase64(partBody, partHeaderInfo.charset);
|
||||
} else if (partHeaderInfo.encoding === 'quoted-printable') {
|
||||
decodedContent = decodeQuotedPrintable(partBody, partHeaderInfo.charset);
|
||||
} else {
|
||||
decodedContent = convertCharset(partBody, partHeaderInfo.charset);
|
||||
}
|
||||
|
||||
// Preserve the original MIME structure
|
||||
if (partHeaderInfo.contentType.includes('text/html')) {
|
||||
content = `
|
||||
<div class="email-part" data-mime-type="text/html" data-charset="${partHeaderInfo.charset}">
|
||||
${decodedContent}
|
||||
</div>
|
||||
`;
|
||||
break;
|
||||
} else if (partHeaderInfo.contentType.includes('text/plain') && !content) {
|
||||
content = `
|
||||
<div class="email-part" data-mime-type="text/plain" data-charset="${partHeaderInfo.charset}">
|
||||
<pre style="white-space: pre-wrap; font-family: inherit;">${decodedContent}</pre>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error decoding email part:', error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle simple email
|
||||
try {
|
||||
let decodedBody = '';
|
||||
if (headerInfo.encoding === 'base64') {
|
||||
decodedBody = decodeBase64(body, headerInfo.charset);
|
||||
} else if (headerInfo.encoding === 'quoted-printable') {
|
||||
decodedBody = decodeQuotedPrintable(body, headerInfo.charset);
|
||||
} else {
|
||||
decodedBody = convertCharset(body, headerInfo.charset);
|
||||
}
|
||||
|
||||
content = `
|
||||
<div class="email-part" data-mime-type="${headerInfo.contentType}" data-charset="${headerInfo.charset}">
|
||||
${headerInfo.contentType.includes('text/html') ? decodedBody : `<pre style="white-space: pre-wrap; font-family: inherit;">${decodedBody}</pre>`}
|
||||
try {
|
||||
const decoded = await decodeEmail(email.body);
|
||||
|
||||
// Format the reply/forward content with proper structure and direction
|
||||
let formattedContent = '';
|
||||
|
||||
if (type === 'forward') {
|
||||
formattedContent = `
|
||||
<div class="forwarded-message">
|
||||
<p>---------- Forwarded message ---------</p>
|
||||
<p>From: ${decoded.from}</p>
|
||||
<p>Date: ${decoded.date.toLocaleString()}</p>
|
||||
<p>Subject: ${decoded.subject}</p>
|
||||
<p>To: ${decoded.to}</p>
|
||||
<br>
|
||||
${decoded.html || `<pre>${decoded.text}</pre>`}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
formattedContent = `
|
||||
<div class="quoted-message">
|
||||
<p>On ${decoded.date.toLocaleString()}, ${decoded.from} wrote:</p>
|
||||
<blockquote>
|
||||
${decoded.html || `<pre>${decoded.text}</pre>`}
|
||||
</blockquote>
|
||||
</div>
|
||||
`;
|
||||
} catch (error) {
|
||||
console.error('Error decoding email body:', error);
|
||||
content = body;
|
||||
}
|
||||
|
||||
return cleanHtml(formattedContent);
|
||||
} catch (error) {
|
||||
console.error('Error generating reply body:', error);
|
||||
return '';
|
||||
}
|
||||
|
||||
// Clean and sanitize HTML content while preserving structure
|
||||
content = cleanHtml(content);
|
||||
|
||||
// Format the reply/forward content with proper structure and direction
|
||||
let formattedContent = '';
|
||||
if (type === 'forward') {
|
||||
formattedContent = `
|
||||
<div class="email-container" dir="ltr">
|
||||
<div class="email-header" dir="ltr">
|
||||
<p class="text-sm text-gray-600 mb-2">Forwarded message from ${email.from}</p>
|
||||
<p class="text-sm text-gray-600 mb-2">Date: ${new Date(email.date).toLocaleString()}</p>
|
||||
<p class="text-sm text-gray-600 mb-2">Subject: ${email.subject}</p>
|
||||
<p class="text-sm text-gray-600 mb-2">To: ${email.to}</p>
|
||||
${email.cc ? `<p class="text-sm text-gray-600 mb-2">Cc: ${email.cc}</p>` : ''}
|
||||
</div>
|
||||
<div class="email-content" dir="auto">
|
||||
${content}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
formattedContent = `
|
||||
<div class="email-container" dir="ltr">
|
||||
<div class="email-header" dir="ltr">
|
||||
<p class="text-sm text-gray-600 mb-2">On ${new Date(email.date).toLocaleString()}, ${email.from} wrote:</p>
|
||||
</div>
|
||||
<div class="email-content" dir="auto">
|
||||
${content}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return formattedContent;
|
||||
}
|
||||
|
||||
export default function CourrierPage() {
|
||||
@ -1170,84 +970,13 @@ export default function CourrierPage() {
|
||||
);
|
||||
};
|
||||
|
||||
const generateEmailPreview = (email: Email): string => {
|
||||
console.log('=== generateEmailPreview Debug ===');
|
||||
console.log('Email ID:', email.id);
|
||||
console.log('Subject:', email.subject);
|
||||
console.log('Body length:', email.body.length);
|
||||
console.log('First 200 chars of body:', email.body.substring(0, 200));
|
||||
|
||||
const generateEmailPreview = async (email: Email): Promise<string> => {
|
||||
try {
|
||||
// Split email into headers and body
|
||||
const [headersPart, ...bodyParts] = email.body.split('\r\n\r\n');
|
||||
const body = bodyParts.join('\r\n\r\n');
|
||||
|
||||
// Parse headers using our MIME decoder
|
||||
const headerInfo = parseEmailHeaders(headersPart);
|
||||
const boundary = extractBoundary(headersPart);
|
||||
|
||||
let preview = '';
|
||||
|
||||
// If it's a multipart email
|
||||
if (boundary) {
|
||||
const parts = body.split(`--${boundary}`);
|
||||
|
||||
for (const part of parts) {
|
||||
if (!part.trim()) continue;
|
||||
|
||||
const [partHeaders, ...partBodyParts] = part.split('\r\n\r\n');
|
||||
const partBody = partBodyParts.join('\r\n\r\n');
|
||||
const partHeaderInfo = parseEmailHeaders(partHeaders);
|
||||
|
||||
if (partHeaderInfo.contentType.includes('text/plain')) {
|
||||
preview = decodeQuotedPrintable(partBody, partHeaderInfo.charset);
|
||||
break;
|
||||
} else if (partHeaderInfo.contentType.includes('text/html') && !preview) {
|
||||
preview = cleanHtml(decodeQuotedPrintable(partBody, partHeaderInfo.charset));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no preview from multipart, try to decode the whole body
|
||||
if (!preview) {
|
||||
preview = decodeQuotedPrintable(body, headerInfo.charset);
|
||||
if (headerInfo.contentType.includes('text/html')) {
|
||||
preview = cleanHtml(preview);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up the preview
|
||||
preview = preview
|
||||
.replace(/^>+/gm, '')
|
||||
.replace(/Content-Type:[^\n]+/g, '')
|
||||
.replace(/Content-Transfer-Encoding:[^\n]+/g, '')
|
||||
.replace(/--[a-zA-Z0-9]+(-[a-zA-Z0-9]+)?/g, '')
|
||||
.replace(/boundary=[^\n]+/g, '')
|
||||
.replace(/charset=[^\n]+/g, '')
|
||||
.replace(/[\r\n]+/g, ' ')
|
||||
.trim();
|
||||
|
||||
// Take first 100 characters
|
||||
preview = preview.substring(0, 100);
|
||||
|
||||
// Try to end at a complete word
|
||||
if (preview.length === 100) {
|
||||
const lastSpace = preview.lastIndexOf(' ');
|
||||
if (lastSpace > 80) {
|
||||
preview = preview.substring(0, lastSpace);
|
||||
}
|
||||
preview += '...';
|
||||
}
|
||||
|
||||
return preview;
|
||||
const decoded = await decodeEmail(email.body);
|
||||
return decoded.text || cleanHtml(decoded.html || '');
|
||||
} catch (error) {
|
||||
console.error('Error generating email preview:', error);
|
||||
return email.body
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/ |‌|»|«|>/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.substring(0, 100)
|
||||
.trim() + '...';
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
71
lib/mail-parser-wrapper.ts
Normal file
71
lib/mail-parser-wrapper.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { simpleParser, ParsedMail, Attachment, HeaderValue, AddressObject } from 'mailparser';
|
||||
|
||||
export interface DecodedEmail {
|
||||
html: string | false;
|
||||
text: string | false;
|
||||
attachments: Attachment[];
|
||||
headers: Map<string, HeaderValue>;
|
||||
subject: string;
|
||||
from: string;
|
||||
to: string;
|
||||
date: Date;
|
||||
}
|
||||
|
||||
function getAddressText(address: AddressObject | AddressObject[] | undefined): string {
|
||||
if (!address) return '';
|
||||
if (Array.isArray(address)) {
|
||||
return address.map(addr => addr.value?.[0]?.address || '').filter(Boolean).join(', ');
|
||||
}
|
||||
return address.value?.[0]?.address || '';
|
||||
}
|
||||
|
||||
export async function decodeEmail(rawEmail: string): Promise<DecodedEmail> {
|
||||
try {
|
||||
const parsed = await simpleParser(rawEmail);
|
||||
|
||||
return {
|
||||
html: parsed.html || false,
|
||||
text: parsed.text || false,
|
||||
attachments: parsed.attachments || [],
|
||||
headers: parsed.headers,
|
||||
subject: parsed.subject || '',
|
||||
from: getAddressText(parsed.from),
|
||||
to: getAddressText(parsed.to),
|
||||
date: parsed.date || new Date()
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error decoding email:', error);
|
||||
throw new Error('Failed to decode email');
|
||||
}
|
||||
}
|
||||
|
||||
export function cleanHtml(html: string): string {
|
||||
if (!html) return '';
|
||||
|
||||
// Detect text direction from the content
|
||||
const hasRtlChars = /[\u0591-\u07FF\u200F\u202B\u202E\uFB1D-\uFDFD\uFE70-\uFEFC]/.test(html);
|
||||
const defaultDir = hasRtlChars ? 'rtl' : 'ltr';
|
||||
|
||||
// Basic HTML cleaning while preserving structure
|
||||
const cleaned = html
|
||||
// Remove script and style tags
|
||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
||||
// Remove meta tags
|
||||
.replace(/<meta[^>]*>/gi, '')
|
||||
// Remove head and title
|
||||
.replace(/<head[^>]*>[\s\S]*?<\/head>/gi, '')
|
||||
.replace(/<title[^>]*>[\s\S]*?<\/title>/gi, '')
|
||||
// Remove body tags
|
||||
.replace(/<body[^>]*>/gi, '')
|
||||
.replace(/<\/body>/gi, '')
|
||||
// Remove html tags
|
||||
.replace(/<html[^>]*>/gi, '')
|
||||
.replace(/<\/html>/gi, '')
|
||||
// Clean up whitespace
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
// Wrap in a div with the detected direction
|
||||
return `<div dir="${defaultDir}">${cleaned}</div>`;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user