AO3 文章下载

一键将 AO3 文章下载为纯文本文件

// ==UserScript==
// @name         AO3 文章下载
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  一键将 AO3 文章下载为纯文本文件
// @author       hydra
// @match        https://archiveofourown.org/works/*/chapters/*
// @match        https://archiveofourown.org/works/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // 仅在文章页面运行(排除评论、收藏等子页面)
    if (!/\/works\/\d+(\/|$)/.test(window.location.pathname)) {
        return;
    }

    // 创建下载按钮
    const button = document.createElement('button');
    button.textContent = '📥 下载为 TXT';
    button.style.cssText = `
        position: fixed;
        top: 10px;
        right: 10px;
        z-index: 10000;
        padding: 8px 12px;
        background: #d4af37;
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        font-weight: bold;
        box-shadow: 0 2px 6px rgba(0,0,0,0.3);
    `;
    button.addEventListener('click', downloadAsTxt);
    document.body.appendChild(button);

    function downloadAsTxt() {
        let txtContent = '';

        // 1. 添加当前 URL
        txtContent += window.location.href + '\n\n';

        // 2. 获取标题
        const titleEl = document.querySelector('h2.title.heading');
        if (titleEl) {
            const title = titleEl.textContent.trim();
            if (title) {
                txtContent += title + '\n\n';
            }
        }


        // 3. 检查是否有章节结构
        const chapterDivs = document.querySelectorAll('div.chapter');

        if (chapterDivs.length > 0) {
            // 多章节
            chapterDivs.forEach(chapter => {
                // 获取章节标题(h3.title)
                const chapterTitleEl = chapter.querySelector('h3.title');
                if (chapterTitleEl) {
                    // 清理可能的链接,只保留文本
                    const chapterTitle = chapterTitleEl.textContent.trim();
                    txtContent += chapterTitle + '\n\n';
                }

                // 获取章节正文(在 userstuff 里,且其 landmark heading 为 "Chapter Text")
                const userstuff = chapter.querySelector('div.userstuff.module[role="article"]');
                if (userstuff) {
                    const chapterText = extractPlainText(userstuff);
                    txtContent += chapterText + '\n\n';
                }
            });
        } else {
            // 单章节:直接找 userstuff(注意可能有多个,但主文通常在外层或第一个)
            const userstuff = document.querySelector('div#chapters div.userstuff, div.userstuff.module[role="article"]');
            if (userstuff) {
                const text = extractPlainText(userstuff);
                txtContent += text + '\n';
            }
        }

        // 4. 下载为文件
        const blob = new Blob([txtContent], { type: 'text/plain;charset=utf-8' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = getSafeFileName(titleEl?.textContent.trim() || 'AO3_Article') + '.txt';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }

    // 提取纯文本并保留段落结构
    function extractPlainText(element) {
        if (!element) return '';

        // 克隆节点避免修改原始页面
        const clone = element.cloneNode(true);

        // 移除不需要的元素(如注释、元数据等)
        const unwanted = clone.querySelectorAll('div.meta, div.notes, div.series, h3.landmark, script, style');
        unwanted.forEach(el => el.remove());

        // 将 <br> 和 block 元素替换为换行
        const blockElements = ['div', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote'];
        blockElements.forEach(tag => {
            const elems = clone.querySelectorAll(tag);
            elems.forEach(el => {
                if (!el.innerHTML.trim()) return;
                el.innerHTML = el.innerHTML.trim() + '\n';
            });
        });

        // 处理 <br> 标签
        const brs = clone.querySelectorAll('br');
        brs.forEach(br => {
            br.parentNode.insertBefore(document.createTextNode('\n'), br);
            br.remove();
        });

        // 获取纯文本并清理多余空行
        let text = clone.textContent || '';
        text = text.replace(/\n\s*\n/g, '\n\n'); // 合并多余空行
        text = text.replace(/^\s+|\s+$/g, '');  // 去首尾空白

        return text;
    }

    // 生成安全的文件名(移除非法字符)
    function getSafeFileName(name) {
        return name
            .replace(/[<>:"/\\|?*\r\n\t]/g, '_')
            .substring(0, 100)
            .trim() || 'AO3_Article';
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址