NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name WTL-LAB Parser
// @description Mejoras para la legibilidad de los capítulos
// @version 2.0.30
// @copyright 2024, trystan4861 (https://openuserjs.org/users/trystan4861)
// @license MIT
// @author trystan4861
// @namespace https://wtr-lab.com/
// @match https://wtr-lab.com/*
// @icon https://wtr-lab.com/images/favicon.png
// @homepageURL https://openuserjs.org/scripts/trystan4861/WTL-LAB_Parser
// @updateURL https://openuserjs.org/meta/trystan4861/WTL-LAB_Parser.meta.js
// @downloadURL https://openuserjs.org/install/trystan4861/WTL-LAB_Parser.user.js
// @grant GM_addStyle
// ==/UserScript==
// ==OpenUserJS==
// @author trystan4861
// ==/OpenUserJS==
/* jshint esversion: 11 */
/*
Change Log:
2.0.30: - 15/02/2025:
* Fix: regexp typo
2.0.29 - 14/02/2025:
* New: Nuevas sustituciones
2.0.28 - 14/02/2025:
* Fix: Refactor tipoFiltro to regexp
2.0.27 - 12/08/2025:
* Refactor
2.0.26 - 12/02/2025:
* New: Sustituir traducción errónea con fanatic()
2.0.25 - 05/02/2025:
* New: Extraer el título del capítulo del contenido del mismo en novelas específicas.
2.0.24 - 11/01/2025:
* Fix: Errores en la carga del script que hacían tener que recargar la página para lanzar el script
2.0.23 - 27/12/2024:
* New: Procesar multiples comas
2.0.22 - 24/12/2024:
* Refactor
* Fix: procear millones con decimales
2.0.21 - 24/12/2024:
* New: Añadir comprobador de novela para ejecutar parseE y parseW
2.0.20 - 24/12/2024:
* Fix: Arreglar fallo en localeNumber que formateaba erróneamente la parte decimal agregando separadores de miles cuando no debía
2.0.19 - 23/12/2024:
* New: Procear magnitudes de millones y de mil millones
2.0.18 - 23/12/2024:
* Fix: Activar el script cuando no se entra directamene a un capítulo y quedarse en espera hasta detectar un
capítulo debido a la forma en que funciona wtr-lab.com para cargar el contenido de las páginas por ajax
2.0.17 - 20/12/2024
* Minor changes
2.0.16 - 16/12/2024:
* Fix: Error en localeNumber
*/
(function () {
'use strict';
const getNovelName = () => (window.location.href.match(/serie-\d+\/([\w-]+)\/chapter-\d+/) || [])[1] || null;
const procesarComas = s => s.replace(/\b(\d+(?:,[0-9]{1,2}){2,})\b/g, match => match.replace(/,/g, ', '));
const procesarPuntos = s => s.replace(/\b(\d+(?:\.\d+){2,})\b/g, match => match.replace(/\./g, ', '));
const fixNumber = s => /^\d+\.\d{2}\.000$/.test(s) ? localeNumber(Number(s.replace(/\./g, '').slice(0, -3)) * 10**2) : s;
const localeNumber = s => String(s).replace(/(\d+)([,.]\d+)?/g, (_, intPart, decimalPart) => intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ".") + (decimalPart || ""));
const parseW = s => s.replace(/(\d+[\.,]?\d*)[wW](?=\b|\.|\+|$)/g, (_, n) => `${parseFloat(n.replace(',', '.')) * 10**4}` );
const parseE = s => s.replace(/(\d+([,|\.]\d+)?)[eE](?![a-zA-Z])/g, (_, n) => parseFloat(n.replace(',', '.') * 10**8));
const tipoFiltro = s => config.filtros.toBreak.some(f => new RegExp(f, "i").test(s)) ? "break" :config.filtros.toDelete.some(f => new RegExp(f, "i").test(s)) ? "delete" : null;
const parseBillions = texto => texto.replace(/(\d+(?:[,.]\d+)?)(\s*(mil millones))/gi, (_, n, t) => parseFloat(n.replace(',', '.') * 10**3).toFixed(0) + " millones");
const parseDecimalMillions = (s) => s.replace(/(\d+),(\d+)\s+millones de\s+(.*)$/, (_, entero, decimales, unidad) => `${parseInt(entero + decimales.padEnd(6, '0'), 10)} ${unidad}`);
const parseMillions = (s) => s.replace(/(\d+),(\d+)\s+millones/, (_, entero, decimales) => localeNumber(entero + decimales.padEnd(6, '0'), 10));
const quitarEspacioEntreNumeroYW = texto => texto.replace(/(\d+(,\d+)?)(\s)W(?![a-zA-Z0-9])/g, (_, p1) => p1 + "W");
const getChapterTitle=()=>document.querySelector(".chapter-title")?.innerText;
const sustituirPalabraSwap = s => s.replace(new RegExp(`(\\w+)\\s+(${Object.keys(store.transforms).join("|")})`, "g"), (_, palabra, termino) => `${store.transforms[termino]} ${palabra}`);
const sustituirPalabra = s => s.replace(new RegExp(`\\b(${Object.keys(store.sustitutes).sort((a, b) => b.length - a.length).map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join("|")})\\b`, "gi"), match => store.sustitutes[match] || match);
const procesarPuntosSuspensivos = s => s.replace(/\.{4,}/g, '...').replace(/…{2,}/g,'…');
const extraerTitulo = p => p.toLowerCase().includes("capítulo") ? (setChapterTitle(p), "deleteThis") : p;
const setChapterTitle=title=> (document.querySelector(".chapter-title").textContent = title)
const fanatic = p => p.replace(/\b(el\s*|los\s*)?(fan(ático(s)?)? de (los\s*)?Xiao)\b/gi, "Xiao Fan");
const config = {
delay: 10000,
estilos: {
fondoResaltado: "yellow",
fondoMarcado: "red",
textoResaltado: "black",
textoMarcado: "black",
},
filtros: {
toHideEqual: [".", "Xinbiquge"],
toHideIncludes: [".c0m", ".com"],
toBreak: ["la versión web es lento", "En el vasto universo, el nacimiento"],
toDelete: ["window\._taboola", "Recordatorio: si descubre que hacer clic","Recordatorio\( cálido\)?: el sitio web está a punto de ser revisado"],
},
novels: {
"a-miracle-at-the-beginning": {
p:[
quitarEspacioEntreNumeroYW,
parseBillions,
parseDecimalMillions,
parseMillions,
procesarPuntosSuspensivos,
],
span:[
parseW,
parseE,
fixNumber,
localeNumber,
sustituirPalabra,
],
},
"son-in-law-god-emperor": {
p:[
fanatic,
sustituirPalabra,
extraerTitulo,
],
}
},
};
const store = {
ultimaPalabraVisible: null,
scrollTimeout: null,
direccionScroll: "down",
ultimaPosicionScroll: window.scrollY,
esPrimeraVez: true,
mutationTimeout: null,
ultimaPalabraMarcada: null,
chapterTitle: "",
sustitutes: {
RPDC: "corte",
Chase: "Perseguidle",
"【Consejo": "【Anuncio",
" 】":"】",
"Blood Mist":"niebla de sangre",
"las focas":"los sellos",
"la foca":"el sello",
"cenario":"ceniciento",
},
transforms: {
Shangshu: "Ministro",
},
};
GM_addStyle(`
.resaltado {
background-color: ${config.estilos.fondoResaltado};
color: ${config.estilos.textoResaltado};
}
.marked {
background-color: ${config.estilos.fondoMarcado};
color: ${config.estilos.textoMarcado};
}
`);
function check2Hide(span) {
const { toHideIncludes, toHideEqual } = config.filtros;
if (toHideIncludes.some(t => span.textContent.toLowerCase().includes(t)) ||
toHideEqual.some(t => span.textContent.trim() === t)) {
span.style.display = "none";
return true;
}
return false;
}
function detectarDireccionScroll() {
const posicionActual = window.scrollY;
store.direccionScroll = posicionActual > store.ultimaPosicionScroll ? "down" : "up";
store.ultimaPosicionScroll = posicionActual;
}
function getSpansFrom(parrafo) {
parrafo.innerHTML = parrafo.textContent.trim()
.split(" ")
.map(palabra => `<span class="marcable">${palabra} </span>`)
.join("");
return Array.from(parrafo.querySelectorAll("span.marcable"));
}
function marcarSpan(span) {
if (store.ultimaPalabraMarcada) {
store.ultimaPalabraMarcada.element.classList.remove("marked");
if (store.ultimaPalabraMarcada.element === span) {
store.ultimaPalabraMarcada = null;
return;
}
}
span.classList.add("marked");
store.ultimaPalabraMarcada = { element: span, tiempoMarcado: Date.now() };
}
function main() {
if (!store.chapterTitle) {
store.chapterTitle = getChapterTitle();
}
if (!store.chapterTitle) return;
store.esPrimeraVez = true;
store.ultimaPalabraVisible = null;
let hideAll = false;
document.querySelectorAll("p").forEach(parrafo => {
const filtro = tipoFiltro(parrafo.textContent.replace('"','~'));
hideAll = hideAll || filtro === "break";
if (hideAll || filtro === "delete") {
parrafo.style.display = "none";
return;
}
parrafo.textContent = parrafo.textContent.trim();
/* aplicar las funciones enumeradas en la variable novels al contenido de los párrafos según sea una novela u otra*/
parrafo.textContent = config.novels[getNovelName()]?.p.reduce((t, f) => f(t), parrafo.textContent) || parrafo.textContent;
if (parrafo.textContent === "deleteThis") {
parrafo.style.display = "none";
return;
}
parrafo.textContent= parrafo.textContent.split(" ").map(palabra=>config.novels[getNovelName()]?.span?.reduce((t,f)=>f(t),palabra) || palabra).join(" ") || parrafo.textContent;
getSpansFrom(parrafo).forEach(span => {
if (check2Hide(span)) return;
observer.observe(span);
span.addEventListener("click", () => marcarSpan(span));
});
});
if (store.ultimaPalabraMarcada && document.contains(store.ultimaPalabraMarcada.element)) {
store.ultimaPalabraMarcada.element.classList.add("marked");
}
}
const doUltimaVisible =(ultimaVisible)=>{
store.ultimaPalabraVisible?.classList.remove("resaltado");
ultimaVisible.classList.add("resaltado");
store.ultimaPalabraVisible = ultimaVisible;
}
const observer = new IntersectionObserver((entradas) => {
const ultimaVisible = entradas.filter(e => e.isIntersecting).at(-1)?.target;
if (!ultimaVisible) return;
if (store.esPrimeraVez) {
doUltimaVisible(ultimaVisible);
store.esPrimeraVez = false;
return;
}
if (store.direccionScroll === "down" && ultimaVisible !== store.ultimaPalabraVisible) {
clearTimeout(store.scrollTimeout);
store.scrollTimeout = setTimeout(() => doUltimaVisible(ultimaVisible), config.delay);
}
}, { threshold: 1.0 });
const mutationObserver = new MutationObserver(() => {
const nuevoTitulo = getChapterTitle();
if (nuevoTitulo !== store.chapterTitle) {
store.chapterTitle = nuevoTitulo;
clearTimeout(store.mutationTimeout);
store.mutationTimeout = setTimeout(main, 100);
}
});
const check=()=>{
if (getChapterTitle()) {
mutationObserver.observe(document.body, { childList: true, subtree: true });
window.addEventListener("scroll", detectarDireccionScroll);
window.addEventListener("load", main);
main();
}
else{
setTimeout(check,2000);
}
}
check();
})();