Hukumusume-Reader

Provides a clean reading view of hukumusume.com

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Hukumusume-Reader
// @namespace    https://zarxrax.github.io/hukumusume-reader/
// @version      1.1.1
// @description  Provides a clean reading view of hukumusume.com
// @match        *://hukumusume.com/douwa/*
// @exclude      *://hukumusume.com/douwa/*itiran*
// @exclude      *://hukumusume.com/douwa/*/index*
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const path = location.pathname.toLowerCase();
    const lastSegment = path.split('/').pop();

    // Skip directory-style pages
    if (!lastSegment || !lastSegment.includes('.')) {
        return;
    }

    const showAllEnabled =
        localStorage.getItem('hukumusume-show-all') === 'true';

    const root = document.body.cloneNode(true);

    const removeAll = (node, selector) => {
        node.querySelectorAll(selector).forEach(el => el.remove());
    };

    const unwrap = el => {
        el.replaceWith(...el.childNodes);
    };

    // --------------------------------------------------
    // Manual fixes for malformed pages
    // --------------------------------------------------

    const brokenPages = [
        'world/06/22.htm',
        'world/07/02.htm'
    ];

    if (brokenPages.some(p => path.includes(p))) {
        root.querySelectorAll('font[size="-1"]').forEach(el => {
            if (el.textContent.includes('イラスト')) {
                unwrap(el);
            }
        });
    }

    function findStoryNode(node) {
        return node.querySelector(
            'td[width="619"], td[width="616"]'
        );
    }

    function cleanGlobal(node) {
        removeAll(
            node,
            'script, style, iframe, noscript, nav, footer, header'
        );

        node.querySelectorAll('*').forEach(el => {
            const src = (el.src || el.href || '').toLowerCase();

            if (src.includes('youtube')) {
                el.remove();
            }
        });
    }

    function cleanStory(node) {
        removeAll(node, 'table');
        removeAll(node, 'font[size="-1"]');

        // Remove reproduction notice links
        node.querySelectorAll('a').forEach(el => {
            if ((el.innerText || '').trim() === '無断転載禁止') {
                const images = el.querySelectorAll('img');
                el.replaceWith(...images);
            }
        });

        // Remove empty/spacer elements
        node.querySelectorAll('p, div, center, td').forEach(el => {
            el.removeAttribute('height');

            const text = (el.innerText || '')
                .replace(/\s| | /g, '');

            const hasImages = el.querySelector('img');

            if (!hasImages && text.length === 0) {
                el.remove();
            }
        });
    }

    function cleanupHtml(html) {
        return html

            // Remove <br> after opening blocks
            .replace(
                /<(p|div|center)([^>]*)>\s*(<br\s*\/?>\s*)+/gi,
                '<$1$2>'
            )

            // Remove <br> before closing blocks
            .replace(
                /(<br\s*\/?>\s*)+<\/(p|div|center)>/gi,
                '</$2>'
            )

            // Collapse excessive <br>
            .replace(
                /(<br\s*\/?>\s*){3,}/gi,
                '<br><br>'
            )

            // Remove empty paragraphs/divs
            .replace(
                /<p>\s*(<br\s*\/?>|\s|&nbsp;)*<\/p>/gi,
                ''
            )

            .replace(
                /<div>\s*(<br\s*\/?>|\s|&nbsp;)*<\/div>/gi,
                ''
            )

            // Remove repeated blank lines
            .replace(/\n\s*\n/g, '\n')

            // Remove leftover text
            .replace(/【睡眠用朗読】/g, '')

            // Fix half-width katakana
            .replace(/イソップ/g, 'イソップ')

            // Remove print-size notice
            .replace(
                /\(画面をクリックすると印刷用の大サイズ\([^)]+\)になります。\)/g,
                ''
            )

            // Remove illustration permission notice
            .replace(
                /※動画のイラストは、福娘童話集が使用許可を出しています。/g,
                ''
            );
    }

    function renderPage(html) {
        document.documentElement.innerHTML = `
            <head>
                <title>${document.title}</title>

                <style>
                    body {
                        background: white;
                        color: black;
                        margin: 0;
                        padding: 0;
                        transition: background 0.2s, color 0.2s;
                    }

                    body.dark-mode {
                        background: #111;
                        color: #eee;
                    }

                    body.dark-mode a {
                        color: #8ab4ff;
                    }

                    #toolbar {
                        position: sticky;
                        top: 0;
                        z-index: 9999;
                        background: #f0f0f0;
                        border-bottom: 1px solid #ccc;
                        padding: 10px;
                        display: flex;
                        gap: 10px;
                        flex-wrap: wrap;
                    }

                    body.dark-mode #toolbar {
                        background: #222;
                        border-color: #444;
                    }

                    #toolbar button {
                        cursor: pointer;
                        padding: 8px 12px;
                        font-size: 14px;
                        border: 1px solid #999;
                        background: white;
                        color: black;
                        border-radius: 6px;
                        transition:
                            background 0.2s,
                            color 0.2s,
                            border-color 0.2s;
                    }

                    body.dark-mode #toolbar button {
                        background: #333;
                        color: #eee;
                        border-color: #666;
                    }

                    body.dark-mode #toolbar button.active {
                        background: #4a90e2;
                        color: white;
                        border-color: #4a90e2;
                    }

                    #toolbar button.active {
                        background: #4a90e2;
                        color: white;
                        border-color: #4a90e2;
                    }

                    #content {
                        max-width: 800px;
                        margin: 10px auto;
                        padding: 20px;
                        font-size: 24px;
                        line-height: 1.8;
                        font-family: serif;
                        white-space: normal;
                    }

                    body.large-font #content {
                        font-size: 48px;
                    }

                    body.hide-images img {
                        display: none !important;
                    }

                    img {
                        max-width: 100%;
                        height: auto;
                    }

                    p, div, center {
                        margin-top: 0.4em;
                        margin-bottom: 0.4em;
                    }
                </style>
            </head>

            <body>
                <div id="toolbar">
                    <button id="toggle-dark">Dark Mode</button>
                    <button id="toggle-font">Larger Text</button>
                    <button id="toggle-images">Hide Images</button>
                    <button id="toggle-show-all">Show All</button>
                </div>

                <div id="content">
                    ${html}
                </div>
            </body>
        `;
    }

    function initToolbar() {
        const toggles = [
            [
                'dark-mode',
                'toggle-dark',
                'hukumusume-dark-mode'
            ],
            [
                'large-font',
                'toggle-font',
                'hukumusume-large-font'
            ],
            [
                'hide-images',
                'toggle-images',
                'hukumusume-hide-images'
            ]
        ];

        toggles.forEach(([cls, id, key]) => {
            const button = document.getElementById(id);

            if (localStorage.getItem(key) === 'true') {
                document.body.classList.add(cls);
                button.classList.add('active');
            }

            button.addEventListener('click', () => {
                document.body.classList.toggle(cls);
                button.classList.toggle('active');

                localStorage.setItem(
                    key,
                    document.body.classList.contains(cls)
                );
            });
        });

        const showAllButton =
            document.getElementById('toggle-show-all');

        if (showAllEnabled) {
            showAllButton.classList.add('active');
        }

        showAllButton.addEventListener('click', () => {
            localStorage.setItem(
                'hukumusume-show-all',
                !showAllEnabled
            );

            location.reload();
        });
    }

    const storyNode = findStoryNode(root);

    if (!storyNode) {
        return;
    }

    if (!showAllEnabled) {
        cleanGlobal(root);
        cleanStory(storyNode);
    }

    let html = storyNode.innerHTML;

    if (!showAllEnabled) {
        html = cleanupHtml(html);
    }

    renderPage(html);

    initToolbar();

})();