import DOMPurify from 'dompurify';
/**
* Parse and sanitize 4chan HTML content
*/
export function parsePostContent(html: string | undefined): string {
if (!html) return '';
// Sanitize HTML to prevent XSS
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['br', 'span', 'a', 'wbr', 's', 'strong', 'em'],
ALLOWED_ATTR: ['class', 'href'],
});
return clean;
}
/**
* Extract quote links from post content
*/
export function extractQuoteLinks(html: string | undefined): number[] {
if (!html) return [];
const quoteRegex = />>(\d+)/g;
const matches = [...html.matchAll(quoteRegex)];
return matches.map((match) => parseInt(match[1], 10));
}
/**
* Check if a line is greentext
*/
export function isGreentext(line: string): boolean {
return line.trim().startsWith('>') && !line.trim().startsWith('>>');
}
/**
* Format post content for display (identify greentext, quote links)
*/
export function formatPostContent(html: string | undefined): {
content: string;
quoteLinks: number[];
hasGreentext: boolean;
} {
if (!html) {
return { content: '', quoteLinks: [], hasGreentext: false };
}
const content = parsePostContent(html);
const quoteLinks = extractQuoteLinks(html);
const lines = html.split('
');
const hasGreentext = lines.some(isGreentext);
return { content, quoteLinks, hasGreentext };
}
/**
* Extract first N characters of plain text from HTML
*/
export function extractPlainText(html: string | undefined, maxLength: number = 200): string {
if (!html) return '';
// Remove HTML tags
const text = html
.replace(/
/gi, '\n')
.replace(/<[^>]+>/g, '')
.replace(/>/g, '>')
.replace(/</g, '<')
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, "'");
if (text.length <= maxLength) return text;
return text.slice(0, maxLength) + '...';
}
/**
* Convert >>12345 style quote links to clickable elements
*/
export function renderQuoteLinks(content: string, onQuoteClick?: (postNo: number) => void): string {
return content.replace(
/>>(\d+)/g,
(match, postNo) => {
return `>>${postNo}`;
}
);
}