// ==UserScript==
// @name taigi-sandhi-visualization
// @namespace hey0wing
// @version 1.5
// @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 GM_setValue
// @grant GM_getValue
// @license MIT
// ==/UserScript==
(async () => {
'use strict';
// Default settings
const isChromeExtension = typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.id;
const isGreasemonkey4Plus = typeof GM !== 'undefined' && typeof GM.setValue !== 'undefined';
const isTampermonkeyCompatible = typeof GM_setValue !== 'undefined';
const setValue = async (key, value) => {
if (isChromeExtension) {
await chrome.storage.local.set({ [key]: value });
} else if (isGreasemonkey4Plus) {
await GM.setValue(key, value);
} else if (isTampermonkeyCompatible) {
GM_setValue(key, value);
} else {
throw new Error('No compatible storage API available');
}
};
const getValue = async (key, defaultValue) => {
if (isChromeExtension) {
return new Promise((resolve) => {
chrome.storage.local.get([key], (result) => {
resolve(result[key] !== undefined ? result[key] : defaultValue);
});
});
} else if (isGreasemonkey4Plus) {
return await GM.getValue(key, defaultValue);
} else if (isTampermonkeyCompatible) {
return Promise.resolve(GM_getValue(key, defaultValue));
} else {
throw new Error('No compatible storage API available');
}
};
var lang = await getValue('lang') || 'zh'
var region = await getValue('region') || 'S'
var syl_id = await getValue('syl_id') || ''
var syl_color = await getValue('syl_color') || ''
function refreshLang() {
const lang_region = document.getElementById('lang_setting')
lang_region.outerHTML = syl_color == 'blue' ?
`
${[["N", "南"], ["S", "北"], ["C", "海"]].map(([en, zh], i) => {
let color = region==en ? 'selected' : 'not_selected'
let val = lang=='zh' ? zh : en
return `
`}).join('')}
${[["zh", "中"], ["en", "Eng"]].map(([k, v], i) => {
let color = lang==k ? 'selected' : 'not_selected'
return `
`}).join('')}
`
}
function refreshDisplay(old_val, new_val) {
const syllableCells = document.getElementsByClassName('tone');
Array.from(syllableCells).forEach(syl => {
const t = syl.dataset[`sandhi_${new_val.toLowerCase()}`]
if (syl_id == `${syl.dataset.tone}_${syl.dataset[`sandhi_${old_val.toLowerCase()}`]}`) {
syl_id = `${syl.dataset.tone}_${t}`
}
syl.innerHTML = t;
});
}
function refreshSandhi() {
const sandhi_diagram = document.getElementById('sandhi_diagram')
if (syl_color == 'red') {
sandhi_diagram.innerHTML = `
${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 '
/
';
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 `
${tone[`sandhi_${region}`]}
${v3}
`;
}).join('
-
');
}).join('
--
');
}).join(' ')}
`
}
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 && !node.classList.contains('syllable-cell')) {
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;
}
.btn.lang, .btn.region {
padding: 0;
font-size: .8rem;
}
.selected {
text-decoration: underline;
}
.not_selected {
color: grey;
}
.tone {
font-size: .8rem;
text-align: center;
}
.tone.red {
color: red;
font-weight: 700;
}
.tone.blue {
color: blue;
font-weight: 700;
}
.tone.green {
color: green;
font-weight: 700;
}
.tone.red:hover, .tone.blue:hover {
cursor: pointer;
background-color: #f0f0f0;
}
`;
document.head.appendChild(style);
const tooltip = document.createElement('div');
tooltip.id = 'custom-tooltip';
tooltip.className = 'custom-tooltip';
tooltip.innerHTML = '