swadhinbiswas Initial release v0.1.0 · 1/16/2026
ed7a4e4
89 lines · 2.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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('<br>');
    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(/<br\s*\/?>/gi, '\n')
        .replace(/<[^>]+>/g, '')
        .replace(/&gt;/g, '>')
        .replace(/&lt;/g, '<')
        .replace(/&amp;/g, '&')
        .replace(/&quot;/g, '"')
        .replace(/&#039;/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(
        /&gt;&gt;(\d+)/g,
        (match, postNo) => {
            return `<span class="quotelink" data-post-no="${postNo}">&gt;&gt;${postNo}</span>`;
        }
    );
}