您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
The userscript converts chinese novel webpage to Vietphrase format to read on web browser
当前为
// ==UserScript== // @name Vietphrase converter // @name:vi convert kiểu Vietphrase // @namespace VP // @version 1.0.2 // @description The userscript converts chinese novel webpage to Vietphrase format to read on web browser // @description:vi convert kiểu Vietphrase để đọc truyện trực tiếp trên web // @author you // @match http*://*/* // @grant GM_setValue // @grant GM_getValue // @run-at document-idle // ==/UserScript== let Options = GM_getValue('Options', { Ngoac: false, Motnghia: true, daucach: ';', DichLieu: true, useSP: false, font: 'Roboto', whiteList: [] //[{host:string,leftRight:boolean, noButton:boolean},{}...] }); let dictNames = GM_getValue('dictNames', undefined); let dictVP = GM_getValue('dictVP', undefined); let dictPA = GM_getValue('dictPA', undefined); let dictSP = GM_getValue('dictSP', undefined); let tmpDictPA; let tmpDictVP; let tmpDictNames; let tmpDictSP; function findNonInline(el) { for (let i = el; i != null; i = i.parentElement) { if (i.tagName == 'IMG' || i.tagName == 'VIDEO') return false; el = i; if (window.getComputedStyle(i)['display'] != 'inline') break; } return el; } //https://github.com/lilydjwg/text-reflow-we function reFlow(e) { const sideMargin = 10 const winWidth = window.visualViewport.width let target = findNonInline(e.target); if (!target) return; const bbox = target.getBoundingClientRect() // if box is wider than screen, reset width to make it fit if (bbox.width > winWidth) { const newWidth = winWidth - (2 * sideMargin) target.style.width = newWidth + 'px' target.__reflowed = true } else if (target.__reflowed) { // don't remove width set by the page itself target.style.width = '' target.__reflowed = false } } function isOverflow(el) { return el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth; } function reflow(el) { const smallestSize = 12; let count = 1; let computedStyle; do { count++; computedStyle = getComputedStyle(el) fontSize = parseInt(computedStyle.fontSize.slice(0, -2)); fontSize = fontSize * .95; el.style.fontSize = fontSize + 'px'; } while (isOverflow(el) && fontSize > smallestSize && count < 10) } function sortSP(a, b) { let cmp = { 'V': 2, 'N': 3 } let aM = a.match(/{\d}|{N\d?}|{V\d?}/g); let bM = b.match(/{\d}|{N\d?}|{V\d?}/g); if (aM.length > bM.length) return -1; if (aM.length < bM.length) return 1; let aS = aM.reduce((s, e) => s += cmp[e.charAt(0)] ?? 0, 0); let bS = bM.reduce((s, e) => s += cmp[e.charAt(0)] ?? 0, 0); if (aS > bS) return -1; if (aS < bS) return 1; return b.length - a.length || a.localeCompare(b); } function str2Dict(str) { let dict = {}; str.trim().split(/\r\n|\r|\n/).forEach(line => { if (/^(\/\/|#|=)/.test(line)) return; //ghi chu let [org, trans] = line.split('='); if (!org || !trans) return; dict[org] = trans.trim(); }) return dict } function transPA(str) { return str.split('').reduce((s, c) => s += dictPA.trans[c] ? (' ' + dictPA.trans[c]) : c, ''); } function transVP(str, Ngoac = true, Motnghia = false, daucach = ';', DichLieu = false) { const _magic = ''; //'\uf0f3' if (dictNames) dictNames.org?.forEach(el => str.replaceAll(el, ' ' + dictNames.trans[el])); if (!dictVP || !dictPA) return str; let result = ''; const maxLength = dictVP.org[0]?.length; const dichlieu = ['的', '了', '着']; for (let i = 0; i < str.length; i++) { for (let j = maxLength; j > 0; j--) { let subStr = str.slice(i, i + j); let VP = dictVP.trans[subStr]; if (typeof VP === 'string' && VP.length > 0) { if (Motnghia) VP = VP.split(daucach)[0]; if (Ngoac) VP = `[${VP.trim()}]`;//Sometimes VP.trim() got error because no trim property/function in VP??? result += ' ' + VP; str.replace(subStr, _magic.repeat(subStr.length)); i += j; } if (j == 1) { if (DichLieu && dichlieu.includes(str.charAt(i))) continue; result += dictPA.trans[str.charAt(i)] ? (' ' + dictPA.trans[str.charAt(i)]) : str.charAt(i); str.replace(str.charAt(i), _magic); } } } return result.replaceAll(/[ ]+/g, ' '); } function transSP1(str) { const regNumber = /{\d}/g if (dictSP.org == undefined) return false; dictSP.org.forEach(sp => { let aC = sp.match(regNumber); let vC = new RegExp(sp.replaceAll(regNumber, '[\\p{sc=Han}、,,0-9]+'), 'ug'); let vV = dictSP.trans[sp]; aC.forEach(ac => vV = vV.replace(ac, `$${aC.indexOf(ac) + 1}`)); str.replaceAll(vC, `<${vV}>`); }) } const transSP = transSP1; function translateNode(rootNode) { let nodeArr = []; let nodesText = ''; const limiter = ''.repeat(2); //'\uf0f5' function nodeToArr(node) { if (node.nodeType == 3) { nodeArr.push(node); nodesText += node.textContent + limiter; } node.childNodes.forEach(childNode => nodeToArr(childNode)) } nodeToArr(rootNode); let translated = transVP(nodesText, Options.Ngoac, Options.Motnghia, Options.daucach, Options.DichLieu); if (translated.length > 25000) { translated.split(limiter).forEach((text, index) => { if (nodeArr[index] == undefined) return; nodeArr[index].textContent = text; }); if (Options.font) document.body.setAttribute('style', `font-family: ${Options.font} !important;`); } else { translated.split(limiter).forEach((text, index) => { if (nodeArr[index] == undefined) return; nodeArr[index].textContent = text; if (Options.font) nodeArr[index].parentElement ? nodeArr[index].parentElement.style = `font-family: ${Options.font} !important` : ''; let el = findNonInline(nodeArr[index].parentElement); if (!el.hasAttribute('reflow') && isOverflow(el)) { reflow(el); el.style.overflow = 'hidden'; el.setAttribute('reflow', ""); for (c of el.children) { let fS = parseInt(getComputedStyle(c).fontSize.slice(0, -2)); if (fS < 12) c.style.fontSize = '12px'; } } }); } } async function fileLoad(event) { let txt = ''; let tmp; if (event.target.files[0]) txt = await event.target.files[0].text(); else return false; switch (event.target.id) { case 'fPA': tmpDictPA = {}; tmpDictPA.trans = str2Dict(txt); break; case 'fVP': tmpDictVP = {}; tmpDictVP.trans = str2Dict(txt); tmpDictVP.org = []; tmpDictVP.org[0] = Object.keys(tmpDictVP.trans).toSorted((a, b) => b.length - a.length || a.localeCompare(b))[0] ?? ''; break; case 'fNames': tmpDictNames = {}; tmpDictNames.trans = str2Dict(txt); tmpDictNames.org = Object.keys(tmpDictNames.trans).toSorted((a, b) => b.length - a.length || a.localeCompare(b)); break; case 'fSP': tmpDictSP = {}; tmpDictSP.trans = str2Dict(txt); tmpDictSP.org = Object.keys(tmpDictSP.trans).toSorted(sortSP); break; } } function addNewSite(ev) { const me = ev.target; const match = me.matches('#usDialog li:last-of-type>input[type="text"]'); if (!me.value && match) return; if (!me.value && !match) me.parentElement.remove(); if (me.value && match) document.querySelector('#usDialog ul').insertAdjacentHTML('beforeend', ` <li><input type="text"> Trái <input type="checkbox" checked> Phải | <input type="checkbox"> Xóa nút</li>`); document.querySelector('#usDialog li:last-of-type>input[type="text"]').addEventListener("change", addNewSite); } (async function () { 'use strict'; if (window.self != window.top) return; if (Options.blackList?.split(/[,;]/).some(e => e.trim() && window.location.host.includes(e.trim()))) return; document.addEventListener('click', reFlow); const auto = Options.whiteList.filter(e => window.location.host.includes(e.host)); if (auto.length > 0) { if (auto[0].leftRight) rightFunc(); else leftFunc(); if (auto[0].noButton) if (auto[0].leftRight) { const observer = new MutationObserver((mL) => { for (const mo of mL) for (const addNode of mo.addedNodes) translateNode(addNode); }); observer.observe(document.body, { childList: true, subtree: true }); } return; } document.body.insertAdjacentHTML('beforeend', ` <style> div.usButton { display: flex; position: fixed; top: 1%; right: 1%; margin: 0; padding: 0; border: thin; z-index: 9999; } div.usButton>button { height: 90%; border: none; margin: 0; text-align: right; } div.usButton>button:first-child { padding: 5px 0px 5px 5px; } div.usButton>button:last-child { padding: 5px 2px 5px 0px; } div.usButton>button:nth-child(2) { padding: 5px 2px 5px 0px; } dialog#usDialog { border: none; border-radius: .3rem; font-family: Arial; padding: .3rem; margin: auto; min-width: 20rem; width: fit-content; } #usDialog>div { display: flex; justify-content: space-around; } #usDialog label:has(#cbMotnghia)+label { display: none; } #usDialog label:has(#cbMotnghia:checked)+label { display: unset; } #usDialog nav>label { width: 4.5rem; display: inline-block; border-radius: 3px 3px 0px 0px; border: 1px solid black; border-bottom: none; text-align: center; } #usDialog nav>label:has(input[type="radio"]:checked) { font-weight: 700; } #usDialog input[type="radio"] { width: 0px; height: 0px; display: none; } #usDialog fieldset { display: none; min-height: 14rem; text-align: left; min-width:fit-content; } #usDialog input[type="text"] { border: 1px solid black; padding:0; } #usDialog textarea { border: 1px solid black; } #usDialog nav:has(#rdTudien:checked)~fieldset:nth-child(2) { display: block; } #usDialog nav:has(#rdDich:checked)~fieldset:nth-child(3) { display: block; } #usDialog nav:has(#rdTudong:checked)~fieldset:nth-child(4) { display: grid; } #usDialog ul { box-sizing: border-box; padding: 0; margin: 0; max-height: 6rem; overflow-y: scroll; } #usDialog li { display: unset; padding: 0; margin: 0; display:block; } #usDialog li>label { display: inline-block; position: relative; border-radius: 1em; width: 2em; height: 1em; background-color: pink; } #usDialog li>label:has(input[type="checkbox"])::before { content: ''; display: unset; position: absolute; left: .15em; top: .1em; border-radius: 50%; width: .8em; height: .8em; background-color: rgb(193, 6, 245); } #usDialog li>label:has(input[type="checkbox"]:checked)::before { display: none; } #usDialog li>label:has(input[type="checkbox"])::after { display: none; } #usDialog li>label:has(input[type="checkbox"]:checked)::after { content: ''; display: unset; position: absolute; border-radius: 50%; right: .15em; top: .1em; width: .8em; height: .8em; background-color: green; } #usDialog li>label>input[type="checkbox"] { width: 0px; height: 0px; display: none; } #usDialog button{ min-width:fit-content; width: 4rem; } </style> <div class="usButton"> <button>Tran</button> <button>slate</button> <button>↓</button> </div> <dialog id="usDialog" spellcheck="false" lang="vie"> <nav> <label id="tudien"><input type="radio" name="groupby" id="rdTudien" checked>Từ điển</label> <label id="cachdich"><input type="radio" name="groupby" id="rdDich">Dịch</label> <label id="tudong"><input type="radio" name="groupby" id="rdTudong">Tự động</label> </nav> <fieldset> <label for="fPA">Phiên Âm <input type="file" id="fPA"></label><br /> <label for="fVP">Vietphrase <input type="file" id="fVP"></label><br /> <label for="fNames">Names <input type="file" id="fNames"></label><br /> <label for="fSP">Strucphrase<input type="file" id="fSP"></label><br /> </fieldset> <fieldset> <label for="cbNgoac"><input type="checkbox" id="cbNgoac"> Dùng [ngoặc]</label><br /> <label for="cbMotnghia"><input type="checkbox" id="cbMotnghia"> Một nghĩa</label> <label for="txtdaucach">, dấu cách nghĩa<input type="text" id="txtdaucach" size="1" maxlength="1"></label><br /> <label for="cbDichLieu"><input type="checkbox" id="cbDichLieu"> Xóa "đích, liễu, trứ"</label><br /> <label for="cbSP"><input type="checkbox" id="cbSP"> Dùng Strucphrase</label><br /> </fieldset> <fieldset> <label for="txtfont" style="width:100%">Font thay thế: </label><input type="text" id="txtfont"> <label for="txtWL" style="width:100%">Các site tự chạy: </label> <ul> </ul> <label for="txtBL" style="width:100%">Bỏ qua các tên miền chứa các chuỗi cách nhau bằng , ;</label> <textarea id="txtBL">.vn;</textarea> </fieldset> <div> <button>OK</button> <button>Cancel</button> </div> </dialog>`); const dialog = document.querySelector('dialog#usDialog'); function leftFunc() { document.title = transPA(document.title); document.body.innerHTML = transVP(document.body.innerHTML, Options.Ngoac, Options.Motnghia, Options.daucach, Options.DichLieu); if (Options.font) document.body.setAttribute('style', `font-family: ${Options.font} !important;`); } function rightFunc() { document.title = transPA(document.title); translateNode(document.body); } document.querySelector('.usButton button:first-child').onclick = leftFunc; document.querySelector('.usButton button:nth-child(2)').onclick = rightFunc; document.querySelector('.usButton button:last-child').onclick = () => { // Menu ↓ button tmpDictPA = undefined; tmpDictVP = undefined; tmpDictNames = undefined; tmpDictSP = undefined; if (dialog.open) dialog.close(); Options = GM_getValue('Options', Options); //sync Options across tabs dialog.querySelectorAll('input[type="file"]').forEach(el => el.value = null); dialog.querySelector('#cbNgoac').checked = Options.Ngoac; dialog.querySelector('#cbMotnghia').checked = Options.Motnghia; dialog.querySelector('#cbDichLieu').checked = Options.DichLieu; dialog.querySelector('#cbSP').checked = Options.useSP; dialog.querySelector('#txtdaucach').value = Options.daucach ?? ';'; dialog.querySelector('#txtfont').value = Options.font ?? ''; const ul = dialog.querySelector('ul'); ul.innerHTML = ''; [...Array.isArray(Options.whiteList) ? Options.whiteList : []].forEach(el => { if (!el || !el.host) return; ul.insertAdjacentHTML('beforeend', ` <li><input type="text" value="${el.host}" > Trái <label><input type="checkbox" ${el.leftRight ? 'checked' : ''}></label> Phải | <input type="checkbox" ${el.noButton ? 'checked' : ''}> Xóa nút</li>`); }); dialog.querySelector('ul').insertAdjacentHTML('beforeend', ` <li><input type="text"> Trái <label><input type="checkbox" checked></label> Phải | <input type="checkbox"> Xóa nút</li>`); dialog.querySelectorAll('li input[type="text"]').forEach(el => el.addEventListener('change', addNewSite)); dialog.querySelector('#txtBL').value = Options.blackList ?? ''; dialog.showModal(); } dialog.querySelectorAll('input[type="file"]').forEach(el => el.onchange = fileLoad); dialog.querySelector('div>button').onclick = () => { //OK button if (tmpDictPA != undefined) { dictPA = tmpDictPA; GM_setValue('dictPA', dictPA) } if (tmpDictVP != undefined) { dictVP = tmpDictVP; GM_setValue('dictVP', dictVP) } if (tmpDictNames != undefined) { dictNames = tmpDictNames; GM_setValue('dictNames', dictNames) } if (tmpDictSP != undefined) { dictSP = tmpDictSP; GM_setValue('dictSP', dictSP) } Options.Ngoac = dialog.querySelector('#cbNgoac').checked; Options.Motnghia = dialog.querySelector('#cbMotnghia').checked; Options.DichLieu = dialog.querySelector('#cbDichLieu').checked; Options.useSP = dialog.querySelector('#cbSP').checked; Options.daucach = dialog.querySelector('#txtdaucach').value.charAt(0) ?? ';'; Options.font = dialog.querySelector('#txtfont').value.trim().split(/[,;]/g)[0]//??'Roboto'; Options.whiteList = []; dialog.querySelectorAll('li').forEach(li => { let host = li.querySelector('input[type="text"]').value; if (host) Options.whiteList.push({ host: host, leftRight: li.querySelector('label>input[type="checkbox"]').checked, noButton: li.querySelector('label+input[type="checkbox"]').checked }) }); Options.blackList = dialog.querySelector('#txtBL').value; GM_setValue('Options', Options); dialog.close(); } dialog.querySelector('div>button:last-child').onclick = () => dialog.close(); //Cancel button })();