Hukumusume-Reader

Provides a clean reading view of hukumusume.com

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 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();

})();