tail-fin-xhs 0.7.8

Xiaohongshu adapter for tail-fin: search, notes, comments, feed
Documentation
pub const JS: &str = r#"(async () => {
    const bodyText = document.body ? document.body.innerText : '';
    const loginWall = /登录后查看|请登录|登录后/.test(bodyText);
    const notFound = /页面不见了|笔记不存在|无法浏览|内容已删除/.test(bodyText);

    if (loginWall || notFound) {
        return { loginWall, notFound, comments: [] };
    }

    // Scroll to trigger comment loading
    const scroller = document.querySelector('.note-scroller') || document.documentElement;
    for (let i = 0; i < 5; i++) {
        scroller.scrollTop += 800;
        await new Promise(r => setTimeout(r, 1000));
    }

    function parseLikes(el) {
        if (!el) return 0;
        const text = (el.textContent || '').trim();
        if (!text || text === '赞' || text === '') return 0;
        const num = parseInt(text.replace(/[^0-9]/g, ''), 10);
        return isNaN(num) ? 0 : num;
    }

    const parentEls = document.querySelectorAll('.parent-comment');
    const comments = Array.from(parentEls).map(parent => {
        const item = parent.querySelector('.comment-item') || parent;
        const authorEl = item.querySelector('.author-wrapper .name, .user-name, .name');
        const textEl = item.querySelector('.content, .note-text');
        const likesEl = item.querySelector('.count, .like-count');
        const timeEl = item.querySelector('.date, .time');

        return {
            author: authorEl ? authorEl.textContent.trim() : '',
            text: textEl ? textEl.textContent.trim() : '',
            likes: parseLikes(likesEl),
            time: timeEl ? timeEl.textContent.trim() : '',
            replies: [],
        };
    });

    return { loginWall: false, notFound: false, comments };
})()
"#;

pub const EXPAND_REPLIES_JS: &str = r#"(async () => {
    // Click all expand buttons for replies
    const expandSelectors = [
        '.reply-container .show-more',
        '.reply-container button',
        '[class*="expand"]',
    ];

    for (const sel of expandSelectors) {
        const buttons = Array.from(document.querySelectorAll(sel));
        for (const btn of buttons) {
            const text = (btn.textContent || '').trim();
            if (/展开|查看更多|more/i.test(text)) {
                btn.click();
                await new Promise(r => setTimeout(r, 800));
            }
        }
    }

    // Wait for content to load
    await new Promise(r => setTimeout(r, 1000));

    function parseLikes(el) {
        if (!el) return 0;
        const text = (el.textContent || '').trim();
        if (!text || text === '赞' || text === '') return 0;
        const num = parseInt(text.replace(/[^0-9]/g, ''), 10);
        return isNaN(num) ? 0 : num;
    }

    function extractComment(item) {
        const authorEl = item.querySelector('.author-wrapper .name, .user-name, .name');
        const textEl = item.querySelector('.content, .note-text');
        const likesEl = item.querySelector('.count, .like-count');
        const timeEl = item.querySelector('.date, .time');
        return {
            author: authorEl ? authorEl.textContent.trim() : '',
            text: textEl ? textEl.textContent.trim() : '',
            likes: parseLikes(likesEl),
            time: timeEl ? timeEl.textContent.trim() : '',
            replies: [],
        };
    }

    const parentEls = document.querySelectorAll('.parent-comment');
    const parentReplies = Array.from(parentEls).map(parent => {
        const replyContainer = parent.querySelector('.reply-container');
        if (!replyContainer) return [];
        const replyEls = replyContainer.querySelectorAll('.comment-item-sub, .comment-item');
        return Array.from(replyEls).map(extractComment);
    });

    return { parentReplies };
})()
"#;