Provides a clean reading view of hukumusume.com
// ==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| )*<\/p>/gi,
''
)
.replace(
/<div>\s*(<br\s*\/?>|\s| )*<\/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();
})();