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();

})();