您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Highlights tone sandhi changes in Taiwanese romanization on the MOE dictionary site. Changed tones are marked in red with a tooltip showing possible base tone → sandhi tone.
// ==UserScript== // @name taigi-sandhi-visualization // @namespace hey0wing // @version 1.2 // @description Highlights tone sandhi changes in Taiwanese romanization on the MOE dictionary site. Changed tones are marked in red with a tooltip showing possible base tone → sandhi tone. // @author hey0wing // @match https://sutian.moe.edu.tw/* // @run-at document-idle // @grant none // @license MIT // ==/UserScript== (() => { 'use strict'; const sandhi_diagram_red = ` <svg id="sandhi_red" width="250" height="150" xmlns="http://www.w3.org/2000/svg"> <!-- Grid of numbers --> <text id="1" x="25" y="25" font-size="12" text-anchor="middle" alignment-baseline="central">1</text> <text id="2" x="125" y="25" font-size="12" text-anchor="middle" alignment-baseline="central">2</text> <text id="4" x="225" y="25" font-size="12" text-anchor="middle" alignment-baseline="central">4</text> <text id="5" x="75" y="75" font-size="12" text-anchor="middle" alignment-baseline="central">5</text> <text id="7" x="25" y="125" font-size="12" text-anchor="middle" alignment-baseline="central">7</text> <text id="3" x="125" y="125" font-size="12" text-anchor="middle" alignment-baseline="central">3</text> <text id="8" x="225" y="125" font-size="12" text-anchor="middle" alignment-baseline="central">8</text> <text id="4h" x="175" y="15" font-size="12" text-anchor="middle" alignment-baseline="central">-h</text> <text id="8h" x="175" y="115" font-size="12" text-anchor="middle" alignment-baseline="central">-h</text> <text id="ptk" x="205" y="75" font-size="12" text-anchor="middle" alignment-baseline="central">-p,t,k</text> <!-- Horizontal arrows --> <path id="2_1" d="M115 25 H35" stroke="black" stroke-width="2" marker-end="url(#arrow)"/> <path id="4_2" d="M215 25 H135" stroke="black" stroke-width="2" marker-end="url(#arrow)"/> <path id="7_3" d="M35 125 H115" stroke="black" stroke-width="2" marker-end="url(#arrow)"/> <path id="8_3" d="M215 125 H135" stroke="black" stroke-width="2" marker-end="url(#arrow)"/> <!-- Vertical arrows --> <path id="1_7" d="M25 35 V115" stroke="black" stroke-width="2" marker-end="url(#arrow)"/> <path id="3_2" d="M125 115 V35" stroke="black" stroke-width="2" marker-end="url(#arrow)"/> <path id="4_8" d="M225 35 V115" stroke="black" stroke-width="2" marker-end="url(#arrow)"/> <path id="8_4" d="M225 115 V35" stroke="black" stroke-width="2" marker-end="url(#arrow)"/> <!-- Diagonal arrows --> <path id="5_7" d="M70 85 L30 120" stroke="black" stroke-width="2" marker-end="url(#arrow)"/> <path id="5_3" d="M80 85 L120 120" stroke="black" stroke-width="2" marker-end="url(#arrow)"/> <!-- arrow definition --> <defs> <marker id="arrow" markerWidth="6" markerHeight="4" refX="5" refY="2" orient="auto"> <polygon points="0 0, 6 2, 0 4" fill="black"/> </marker> <marker id="arrow_red" markerWidth="6" markerHeight="4" refX="5" refY="2" orient="auto"> <polygon points="0 0, 6 2, 0 4" fill="red"/> </marker> </defs> </svg> ` const sandhi_diagram_blue = ` <svg id="sandhi_blue" width="250" height="150" xmlns="http://www.w3.org/2000/svg"> <!-- Grid of numbers --> <text id="2,3" x="25" y="25" font-size="12" text-anchor="middle" alignment-baseline="central">2,3</text> <text id="1" x="125" y="25" font-size="12" text-anchor="middle" alignment-baseline="central">1</text> <text id="4" x="225" y="25" font-size="12" text-anchor="middle" alignment-baseline="central">4</text> <text id="5" x="25" y="125" font-size="12" text-anchor="middle" alignment-baseline="central">5</text> <text id="7" x="125" y="125" font-size="12" text-anchor="middle" alignment-baseline="central">7</text> <text id="8" x="225" y="125" font-size="12" text-anchor="middle" alignment-baseline="central">8</text> <text id="4h" x="175" y="15" font-size="12" text-anchor="middle" alignment-baseline="central">-h</text> <text id="8h" x="175" y="115" font-size="12" text-anchor="middle" alignment-baseline="central">-h</text> <text id="ptk" x="205" y="75" font-size="12" text-anchor="middle" alignment-baseline="central">-p,t,k</text> <!-- Horizontal arrows --> <path id="2,3_1" d="M40 25 H115" stroke="black" stroke-width="2" marker-end="url(#arrow)"/> <path id="4_1" d="M215 25 H135" stroke="black" stroke-width="2" marker-end="url(#arrow)"/> <path id="5_7" d="M35 125 H115" stroke="black" stroke-width="2" marker-end="url(#arrow)"/> <path id="8_7" d="M215 125 H135" stroke="black" stroke-width="2" marker-end="url(#arrow)"/> <!-- Vertical arrows --> <path id="1_7" d="M125 35 V115" stroke="black" stroke-width="2" marker-end="url(#arrow)"/> <path id="4_8" d="M225 35 V115" stroke="black" stroke-width="2" marker-end="url(#arrow)"/> <path id="8_4" d="M225 115 V35" stroke="black" stroke-width="2" marker-end="url(#arrow)"/> <!-- arrow definition --> <defs> <marker id="arrow" markerWidth="6" markerHeight="4" refX="5" refY="2" orient="auto"> <polygon points="0 0, 6 2, 0 4" fill="black"/> </marker> <marker id="arrow_blue" markerWidth="6" markerHeight="4" refX="5" refY="2" orient="auto"> <polygon points="0 0, 6 2, 0 4" fill="blue"/> </marker> </defs> </svg> ` let sandhi_isH = { // suffix === á true: { 8: 5, 4: 2 }, // suffix !== á false: { 8: 3, 4: 2 }, } let sandhi_map = { // suffix === á true: { 1: 7, 2: 1, 3: 1, 5: 7, 7: 7, 4: 8, 8: 4 }, // suffix !== á false: { 1: 7, 2: 1, 3: 2, 5: 7, 7: 3, 4: 8, 8: 4 }, } // Function to get the tone number from a syllable function getTone({syllable='', sandhi='', suffix='', neutral=null}) { const isChecked = /[pthk]\.?$/.test(syllable); const isH = /[h]$/.test(syllable); const normalized = syllable.normalize('NFD'); let tone = null; for (let i = 0; i < normalized.length; i++) { const code = normalized.charCodeAt(i); if (code >= 0x0300 && code <= 0x036F) { // Combining diacritics tone = code === 0x0301 ? 2 : code === 0x0300 ? 3 : code === 0x0302 ? 5 : code === 0x0304 ? 7 : code === 0x030D ? 8 : code === 0x030C ? 6 : code === 0x030B && 9 } } tone ??= isChecked ? 4 : 1 if (neutral=='before') return { tone: tone, sandhi: null, color: 'green', display: tone } if (neutral=='after') return { tone: 0, sandhi: null, color: 'green', display: 0 } let sandhi_t = (tone != 6 && tone != 9 && sandhi) ? (isH ? sandhi_isH[suffix == 'á'][tone] : sandhi_map[suffix == 'á'][tone]) : null return { tone: tone, sandhi: sandhi_t, color: sandhi_t ? (suffix == 'á' ? 'blue': 'red') : null, display: sandhi_t ? sandhi_t : tone, } } // Modified from https://github.com/andreihar/taibun.js function isCjk(input) { return [...input].some(char => { const code = char.codePointAt(0); return ( (0x4E00 <= code && code <= 0x9FFF) || // BASIC (0x3400 <= code && code <= 0x4DBF) || // Ext A (0x20000 <= code && code <= 0x2A6DF) || // Ext B (0x2A700 <= code && code <= 0x2EBEF) || // Ext C,D,E,F (0x30000 <= code && code <= 0x323AF) || // Ext G,H (0x2EBF0 <= code && code <= 0x2EE5F) // Ext I ); }); } function highlightSandhi(text) { const words = text.replace('/',' / ').split(/\s+/); return ` <div class="d-flex flex-row flex-wrap align-items-end"> ${words.map((v1, i) => { let w1 = v1.split('--'); return w1.map((v2, j) => { let word = v2.split('-'); return word.map((v3, k) => { if (v3 === '/') return '<div>/</div>'; let tone; if (k === word.length - 1 && j !== w1.length - 1) { // Word before 輕聲 neutral tone tone = getTone({ syllable: v3, neutral: 'before' }); } else if (k === 0 && j !== 0) { // Word after 輕聲 neutral tone tone = getTone({ syllable: v3, neutral: 'after' }); } else if (word.length === 1 && i !== words.length - 1 && ![',','.','!'].some(x => v3.includes(x))) { // Monosyllabic and not the final word tone = getTone({ syllable: v3, sandhi: true, suffix: words[i + 1] }); } else { tone = getTone({ syllable: v3, sandhi: k !== word.length - 1, suffix: word[k + 1] }); } return ` <div> <div class="syllable-cell ${tone.color}" data-color="${tone.color}" data-tone="${tone.tone}" data-sandhi="${tone.sandhi}"> ${tone.display} </div> <div>${v3}</div> </div> `; }).join('<div>-</div>'); }).join('<div>--</div>'); }).join(' ')} </div>` } function processPage(node) { if (node.nodeType === Node.TEXT_NODE) { let parent = node.parentNode; while (parent) { parent = parent.parentNode; } let text = node.nodeValue.trim(); if (text && /[\-āáàâǎa̍ēéèêěe̍īíìîǐi̍ōóòôǒo̍ūúùûǔu̍͘]/.test(text) && !isCjk(text)) { const div = document.createElement('div'); div.innerHTML = highlightSandhi(text); node.parentNode.replaceChild(div, node); } } else if (node.nodeType === Node.ELEMENT_NODE) { if (node.tagName === 'UL' && ['fs-4', 'fw-bold', 'list-inline'].every(c => node.classList.contains(c))) { replaceUL(node); } else { for (let i = 0; i < node.childNodes.length; i++) { processPage(node.childNodes[i]); } } } return true } function replaceUL(node) { // Get all span texts from li children and join with "/" let spanTexts = Array.from(node.querySelectorAll('li')) .map(li => li.querySelector('span')?.textContent || '') .filter(text => text) .join('/'); node.querySelectorAll('span').forEach(span => span.remove()); node.parentNode.classList.remove('align-items-baseline'); node.parentNode.classList.add('align-items-end'); node.lastChild.classList.remove('slash-divider'); const div = document.createElement('li'); div.innerHTML = highlightSandhi(spanTexts); div.classList.add('list-inline-item'); node.insertBefore(div, node.firstChild); } const style = document.createElement('style'); style.textContent = ` .custom-tooltip { display: none; position: absolute; background-color: white; padding: 5px 10px; border: 4px solid black; border-radius: 4px; font-size: 12px; z-index: 100; } .syllable-cell { font-size: .8rem; text-align: center; } .syllable-cell.red { color: red; font-weight: 700; } .syllable-cell.blue { color: blue; font-weight: 700; } .syllable-cell.green { color: green; font-weight: 700; } .syllable-cell.red:hover, .syllable-cell.blue:hover { cursor: pointer; background-color: #f0f0f0; } `; document.head.appendChild(style); const tooltip = document.createElement('div'); tooltip.id = 'custom-tooltip'; tooltip.className = 'custom-tooltip'; document.body.appendChild(tooltip); document.addEventListener('click', (e) => { const tooltip = document.getElementById('custom-tooltip'); if (e.target.classList.contains('syllable-cell') && e.target.dataset.sandhi != 'null') { tooltip.style.display = 'block'; tooltip.innerHTML = e.target.dataset.color == 'red' ? sandhi_diagram_red : sandhi_diagram_blue var id = `${e.target.dataset.tone}_${e.target.dataset.sandhi}`; if (id == '4_8') document.getElementById('8_4').remove(); if (id == '8_4') document.getElementById('4_8').remove(); if (['2_1', '3_1'].includes(id) && e.target.dataset.color == 'blue') id = '2,3_1' if (id == '7_7') { const text = document.getElementById('7'); text.setAttribute('fill', e.target.dataset.color); text.setAttribute('font-size', 16); } else { const path = document.getElementById(id); path.setAttribute('stroke', e.target.dataset.color); path.setAttribute('marker-end', `url(#arrow_${e.target.dataset.color})`); } const rect = e.target.getBoundingClientRect(); let left = rect.left + window.scrollX + rect.width / 2 - tooltip.offsetWidth / 2; let top = rect.top + window.scrollY - tooltip.offsetHeight - 10; left = Math.max(0, Math.min(left, window.innerWidth - tooltip.offsetWidth)); top = top < 0 ? rect.top + window.scrollY + rect.height + 10 : top; tooltip.style.left = `${left}px`; tooltip.style.top = `${top}px`; } else { tooltip.style.display = 'none'; tooltip.innerHTML = '' } }); // Run initially and observe for changes console.log("taigi-sandhi-visualization") if (processPage(document.getElementsByTagName('main')[0])) { console.log('done') } const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE) { if (processPage(node)) { console.log('done') } } }); }); }); observer.observe(document.getElementsByTagName('main')[0], { childList: true, subtree: true }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址