Skip to content

Instantly share code, notes, and snippets.

@SchmidtDavid
Created September 1, 2025 15:59
Show Gist options
  • Save SchmidtDavid/d8edd2a64144c59744d9ce3b58c75927 to your computer and use it in GitHub Desktop.
Save SchmidtDavid/d8edd2a64144c59744d9ce3b58c75927 to your computer and use it in GitHub Desktop.
Fixes: Preloads images right away (especially nice on mokuro.moe - the images start loading basically instantly on my computer/web connection) Combine nearby text boxes so that mokuro stops splitting sentences&words in the middle (enables better parsing, copy paste, etc) Consistent font & font size for the japanese text, higher contrast. Vertica…
// ==UserScript==
// @name Better Mokuro
// @version 1.0
// @description Enhances Mokuro-generated manga HTML files by preloading images, merging nearby text boxes, applying consistent Japanese font and higher contrast, and enabling vertical scrolling. Disable Migaku before running.
// @author David Schmidt
// @match *://*.mokuro.moe/*
// @note Also works on local Mokuro HTML files (file://) when run in the console
// @run-at document-end
// @license CC0-1.0
// @namespace https://gist.github.com/
// ==/UserScript==
(() => {
console.log("Adding Better Mokuro activation button...");
// Remove any existing button
const existingButton = document.getElementById('activateBetterMokuro');
if (existingButton) existingButton.remove();
// Create the activation button
const activateButton = document.createElement('button');
activateButton.id = 'activateBetterMokuro';
activateButton.innerHTML = '🚀 Activate Better Mokuro';
activateButton.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10000;
padding: 20px 30px;
background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 12px;
cursor: pointer;
font-size: 18px;
font-weight: bold;
box-shadow: 0 8px 25px rgba(0,0,0,0.3);
transition: all 0.3s ease;
`;
activateButton.onmouseover = () => {
activateButton.style.transform = 'translate(-50%, -50%) scale(1.05)';
activateButton.style.boxShadow = '0 12px 35px rgba(0,0,0,0.4)';
};
activateButton.onmouseout = () => {
activateButton.style.transform = 'translate(-50%, -50%) scale(1)';
activateButton.style.boxShadow = '0 8px 25px rgba(0,0,0,0.3)';
};
// The entire mokuro conversion script runs when button is clicked
activateButton.onclick = () => {
console.log("🚀 Activating Better Mokuro...");
// Remove the activation button
activateButton.remove();
// THE ENTIRE MOKURO SCRIPT STARTS HERE
console.log("Converting mokuro and merging p tags within text boxes...");
// Force single page mode if state exists
if (typeof state !== 'undefined') {
state.r2l = false;
state.singlePageView = true;
state.hasCover = false;
state.pagesDisplayed = 1;
}
// Track loaded pages to prevent reloading
const loadedPages = new Set();
// Simple function to merge p tags within a text box
function mergePTagsInTextBox(textBox) {
const pTags = textBox.querySelectorAll('p');
if (pTags.length <= 1) return; // Nothing to merge
// Extract all text content from p tags
const allTexts = Array.from(pTags).map(p => p.textContent.trim()).filter(text => text);
if (allTexts.length === 0) return;
// Merge text content, removing spaces and line breaks
const mergedText = allTexts.join('').replace(/\s+/g, '').replace(/\n+/g, '');
// Remove all existing p tags
pTags.forEach(p => p.remove());
// Create single new p tag with merged content
const newP = document.createElement('p');
newP.textContent = mergedText;
textBox.appendChild(newP);
console.log(`📝 Merged ${pTags.length} p tags: "${mergedText}"`);
}
// Collect pages with both images and text boxes
const pages = [];
const seen = new Set();
document.querySelectorAll('.pageContainer').forEach(container => {
const style = container.getAttribute('style') || '';
const match = style.match(/background-image:\s*url\(["']?([^"')]+)["']?\)/i);
if (match && match[1]) {
const imageUrl = match[1];
if (!seen.has(imageUrl)) {
seen.add(imageUrl);
// Get all text boxes in this page container
const textBoxes = Array.from(container.querySelectorAll('.textBox')).map(textBox => {
const computedStyle = window.getComputedStyle(textBox);
const clonedBox = textBox.cloneNode(true);
// Merge p tags within this text box
mergePTagsInTextBox(clonedBox);
return {
element: clonedBox,
left: parseFloat(computedStyle.left) || 0,
top: parseFloat(computedStyle.top) || 0,
width: parseFloat(computedStyle.width) || 0,
height: parseFloat(computedStyle.height) || 0,
originalStyle: textBox.getAttribute('style') || ''
};
});
const containerComputedStyle = window.getComputedStyle(container);
pages.push({
imageUrl,
textBoxes,
containerWidth: parseFloat(containerComputedStyle.width) || 800,
containerHeight: parseFloat(containerComputedStyle.height) || 1200,
originalContainer: container
});
}
}
});
if (pages.length === 0) {
console.error("No pages found!");
return;
}
console.log(`Found ${pages.length} pages`);
// Hide existing containers
const pagesContainer = document.getElementById('pagesContainer');
if (pagesContainer) {
pagesContainer.style.display = 'none';
}
// Create vertical container
const verticalContainer = document.createElement('div');
verticalContainer.id = 'verticalMangaContainer';
verticalContainer.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
background: #000;
z-index: 1000;
display: flex;
flex-direction: column;
align-items: center;
padding: 0;
box-sizing: border-box;
scroll-behavior: smooth;
`;
// Add CSS with universal font sizing
const styleSheet = document.createElement('style');
styleSheet.textContent = `
#verticalMangaContainer .textBox {
position: absolute;
pointer-events: auto;
cursor: text;
user-select: text;
z-index: 10;
border-radius: 0.25em;
}
#verticalMangaContainer .textBox p {
opacity: 1;
background: rgba(255, 255, 255, 0.95);
color: rgb(0, 0, 0);
margin: 0;
padding: 0.25em;
border-radius: 0.25em;
border: 0.125em solid rgba(0, 0, 0, 0.2);
line-height: 1.3;
word-wrap: break-word;
overflow-wrap: break-word;
font-family: 'Noto Sans CJK JP', 'Yu Gothic', 'Hiragino Kaku Gothic ProN', 'Meiryo', sans-serif;
font-size: 18px !important;
font-weight: 400;
transition: all 0.2s ease;
}
#verticalMangaContainer .textBox:hover p {
background: rgba(255, 255, 255, 1);
color: rgb(0, 0, 0);
border: 0.125em solid rgba(0, 0, 0, 0.4);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
transform: scale(1.02);
}
`;
document.head.appendChild(styleSheet);
// Create pages with images and text overlays
pages.forEach((page, pageIndex) => {
const pageContainer = document.createElement('div');
pageContainer.style.cssText = `
position: relative;
width: 100%;
max-width: 100vw;
display: flex;
justify-content: center;
background: #000;
`;
pageContainer.dataset.pageNumber = pageIndex + 1;
const img = document.createElement('img');
img.src = page.imageUrl;
img.alt = `Page ${pageIndex + 1}`;
img.style.cssText = `
max-width: 100%;
max-height: 100vh;
width: auto;
height: auto;
display: block;
object-fit: contain;
`;
// Handle image load to position text boxes correctly
img.onload = () => {
const pageKey = `${pageIndex + 1}`;
if (loadedPages.has(pageKey)) {
return; // Already loaded, don't process again
}
loadedPages.add(pageKey);
console.log(`Loaded page ${pageIndex + 1}`);
// Calculate scaling factors
const imgRect = img.getBoundingClientRect();
const scaleX = imgRect.width / page.containerWidth;
const scaleY = imgRect.height / page.containerHeight;
// Position text boxes relative to the image
page.textBoxes.forEach((textBoxData) => {
const textBox = textBoxData.element;
// Calculate new position relative to the image
const newLeft = textBoxData.left * scaleX;
const newTop = textBoxData.top * scaleY;
const newWidth = textBoxData.width * scaleX;
const newHeight = textBoxData.height * scaleY;
// Position relative to the page container
const pageContainerRect = pageContainer.getBoundingClientRect();
const imgOffsetX = (pageContainerRect.width - imgRect.width) / 2;
textBox.style.cssText = `
position: absolute;
left: ${imgOffsetX + newLeft}px;
top: ${newTop}px;
width: ${newWidth}px;
height: ${newHeight}px;
z-index: 10;
`;
// Set universal font size for the p tag
const pTag = textBox.querySelector('p');
if (pTag) {
const baseFontSize = 18;
const scaledFontSize = baseFontSize * Math.min(scaleX, scaleY);
pTag.style.fontSize = `${scaledFontSize}px !important`;
pTag.style.fontFamily = "'Noto Sans CJK JP', 'Yu Gothic', 'Hiragino Kaku Gothic ProN', 'Meiryo', sans-serif";
pTag.style.fontWeight = "400";
}
// Preserve original classes
textBox.className = (textBoxData.element.className + ' textBox').trim();
pageContainer.appendChild(textBox);
});
};
img.onerror = () => {
console.error(`Failed to load page ${pageIndex + 1}`);
};
pageContainer.appendChild(img);
verticalContainer.appendChild(pageContainer);
});
// Simple navigation
const closeButton = document.createElement('button');
closeButton.textContent = '✕ Restore';
closeButton.style.cssText = `
position: fixed;
top: 10px;
right: 10px;
z-index: 1001;
padding: 8px 12px;
background: rgba(0,0,0,0.8);
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 12px;
`;
closeButton.onclick = () => {
styleSheet.remove();
verticalContainer.remove();
closeButton.remove();
if (pagesContainer) pagesContainer.style.display = '';
location.reload();
};
// Disable existing mokuro behavior
if (typeof pz !== 'undefined' && pz.dispose) {
try {
pz.dispose();
} catch (e) {
console.log("Could not dispose panzoom:", e);
}
}
const functionsToDisable = [
'nextPage', 'prevPage', 'firstPage', 'lastPage',
'updatePage', 'zoomFitToScreen', 'zoomFitToWidth', 'panAlign'
];
functionsToDisable.forEach(funcName => {
if (typeof window[funcName] === 'function') {
window[funcName] = () => console.log(`${funcName} disabled in vertical mode`);
}
});
// Add keyboard navigation
verticalContainer.addEventListener('keydown', (e) => {
const scrollAmount = window.innerHeight * 0.8;
switch(e.key) {
case 'ArrowDown':
case ' ':
case 'PageDown':
e.preventDefault();
verticalContainer.scrollBy({ top: scrollAmount, behavior: 'smooth' });
break;
case 'ArrowUp':
case 'PageUp':
e.preventDefault();
verticalContainer.scrollBy({ top: -scrollAmount, behavior: 'smooth' });
break;
case 'Home':
e.preventDefault();
verticalContainer.scrollTo({ top: 0, behavior: 'smooth' });
break;
case 'End':
e.preventDefault();
verticalContainer.scrollTo({ top: verticalContainer.scrollHeight, behavior: 'smooth' });
break;
case 'Escape':
closeButton.click();
break;
}
});
// Add to page
document.body.appendChild(verticalContainer);
document.body.appendChild(closeButton);
// Focus for keyboard navigation
verticalContainer.tabIndex = -1;
verticalContainer.focus();
console.log("🎯 Done! Merged multiple p tags within text boxes into single p tags");
// END OF MOKURO SCRIPT
};
// Add button to page
document.body.appendChild(activateButton);
console.log("✅ Activation button added! Close console, then click the big button to activate Better Mokuro.");
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment