Hukumusume-Reader

Provides a clean reading view of hukumusume.com

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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

})();