Hukumusume-Reader

Provides a clean reading view of hukumusume.com

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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

})();