[chordwiki] コード to ディグリー

ja.chordwiki.orgのキーが明記されいるページのコード名をディグリーに変換

当前为 2025-08-17 提交的版本,查看 最新版本

// ==UserScript==
// @name			[chordwiki] コード to ディグリー
// @description			ja.chordwiki.orgのキーが明記されいるページのコード名をディグリーに変換
// @namespace		https://gf.qytechs.cn/ja/users/1023652
// @version			0.0.0.6
// @author			ゆにてぃー
// @match			https://ja.chordwiki.org/wiki*
// @icon			https://www.google.com/s2/favicons?sz=64&domain=ja.chordwiki.org
// @license			MIT
// @grant			GM_xmlhttpRequest
// @grant			GM_registerMenuCommand
// ==/UserScript==

(async function(){
	'use strict';
	let currentUrl = document.location.href;
	let updating = false;
	const debugging = true;
	const debug = debugging ? console.log : ()=>{};
	const userAgent = navigator.userAgent || navigator.vendor || window.opera;

	async function main(){
		addConvertFab();
		hookPlayKeyObserver(); // Play: が変わったら自動更新
	}

	let isDeg = false;

	// ===== 右下固定の丸ボタン(FAB) =====
	function addConvertFab(){
		if(document.getElementById("cw-degree-fab"))return;
		const btn = h('button',{
			id:"cw-degree-fab",
			title:"コード名 ↔ ディグリー",
			onClick:()=>{
				const hasKey = !!document.querySelector('p.key');
				if(!hasKey){alert("キーがわからない");return;}
				if(!isDeg){
					convertDocument("deg");
					isDeg = true;
					btn.textContent = "C";
				}else{
					convertDocument("orig");
					isDeg = false;
					btn.textContent = "Ⅰ";
				}
			},
			textContent:"Ⅰ",
			style:{
				position:'fixed',
				right:'16px',
				bottom:'16px',
				width:'56px',
				height:'56px',
				borderRadius:'9999px',
				zIndex:'2147483647',
				border:'none',
				cursor:'pointer',
				boxShadow:'0 6px 16px rgba(0,0,0,.25)',
				background:'#ffffff',
				fontSize:'20px',
				lineHeight:'56px',
				textAlign:'center',
				userSelect:'none'
			}
		});
		btn.addEventListener('mouseenter',()=>{
			btn.style.transform = 'scale(1.06)';
			btn.style.boxShadow = '0 10px 24px rgba(0,0,0,.30)';
		});
		btn.addEventListener('mouseleave',()=>{
			btn.style.transform = '';
			btn.style.boxShadow = '0 6px 16px rgba(0,0,0,.25)';
		});
		document.body.appendChild(btn);
	}

	// ===== ローマ数字基礎 =====
	const ROMAN = ["Ⅰ","Ⅱ","Ⅲ","Ⅳ","Ⅴ","Ⅵ","Ⅶ"];

	function buildLetterOrder(key){
		const L = ["C","D","E","F","G","A","B"];
		const k = (key || "").toUpperCase().match(/^([A-G])/ )?.[1] || "C";
		const i = L.indexOf(k);
		return i < 0 ? L.slice() : L.slice(i).concat(L.slice(0,i));
	}

	function buildRomanMap(key){
		const order = buildLetterOrder(key);
		const m = {};
		for(let i = 0;i < order.length;i++){
			m[order[i]] = ROMAN[i];
		}
		return m;
	}

	// ===== キーの調号マップ(文字ごとに # / b / なし) =====
	function normalizeKeyName(key){
		const m = (key || "").trim().toUpperCase().match(/^([A-G])([#B])?$/);
		if(!m)return "C";
		const letter = m[1];
		const acc = m[2] === "B" ? "b" : (m[2] || "");
		return letter + acc;
	}

	function buildKeyAccidentalMap(key){
		const k = normalizeKeyName(key);
		const SHARP_KEYS = ["C","G","D","A","E","B","F#","C#"];
		const FLAT_KEYS = ["C","F","Bb","Eb","Ab","Db","Gb","Cb"];
		const SHARP_ORDER = ["F","C","G","D","A","E","B"];
		const FLAT_ORDER = ["B","E","A","D","G","C","F"];
		const map = {C:"",D:"",E:"",F:"",G:"",A:"",B:""};
		let idxSharp = SHARP_KEYS.indexOf(k);
		let idxFlat = FLAT_KEYS.indexOf(k);
		if(idxSharp >= 0){
			for(let i = 0;i < idxSharp;i++)map[SHARP_ORDER[i]] = "#";
		}else if(idxFlat >= 0){
			for(let i = 0;i < idxFlat;i++)map[FLAT_ORDER[i]] = "b";
		}
		return map;
	}

	// 単音 → ローマ数字(キーの調号と比較して相対臨時記号を付与)
	function noteToDegree(note,romanMap,keyAcc){
		const m = (note || "").match(/^([A-G])([#b])?$/i);
		if(!m)return note;
		const letter = m[1].toUpperCase();
		const acc = m[2] || "";
		const base = romanMap[letter] || letter;
		const dia = keyAcc[letter] || "";
		let rel = "";
		if(acc === dia)rel = "";
		else if(acc === "" && dia === "#")rel = "b";
		else if(acc === "" && dia === "b")rel = "#";
		else if(acc === "#" && dia === "")rel = "#";
		else if(acc === "b" && dia === "")rel = "b";
		else if(acc === "#" && dia === "b")rel = "##";
		else if(acc === "b" && dia === "#")rel = "bb";
		return base + rel;
	}

	// コード全体:"F#m7-5" "C#7(b9)" "Aadd9/B" "Baug/F"
	function convertChordSymbol(sym,romanMap,keyAcc){
		const s = (sym || "").trim();
		if(s === "N.C.")return s;
		const mSlash = s.match(/^\/\s*([A-G](?:#|b)?)\s*$/i);
		if(mSlash){
			if(!romanMap || !keyAcc)return s; // キー未確定なら元のまま
			const bass = mSlash[1];
			const bassDeg = noteToDegree(bass,romanMap,keyAcc);
			return "/" + bassDeg;
		}
		const re = /^([A-G](?:#|b)?)(.*?)(?:\/([A-G](?:#|b)?))?$/i;
		const m = s.match(re);
		if(!m)return s;

		const root = m[1], suffix = m[2] || "", bass = m[3] || "";
		const rootDeg = noteToDegree(root,romanMap,keyAcc);
		const bassDeg = bass ? noteToDegree(bass,romanMap,keyAcc) : "";
		return rootDeg + suffix + (bassDeg ? ("/" + bassDeg) : "");
	}
	// ===== Key 抽出(Play優先) =====
	function extractEffectiveKey(text){
		const t = (text || "").trim();
		let play = null, orig = null, key = null;

		// 分割して順に見る(" / "や"/"、縦線区切りにも緩く対応)
		const parts = t.split(/[\/|\|]/);

		for(let i = 0;i < parts.length;i++){
			const seg = parts[i];
			let m = null;

			// Play
			m = seg.match(/Play[::]\s*([A-G](?:#|b)?)/i);
			if(m)play = m[1];

			// Original Key / 原曲キー
			m = seg.match(/Original\s*[Kk]ey[::]\s*([A-G](?:#|b)?)/i);
			if(m)orig = m[1];
			m = seg.match(/原曲キー[::]\s*([A-G](?:#|b)?)/i);
			if(m)orig = m[1];

			// Key / キー(単独のKeyのみ拾う意図で別枠)
			m = seg.match(/(?<!Original\s)[Kk]ey[::]\s*([A-G](?:#|b)?)/);
			if(m)key = m[1];
			m = seg.match(/キー[::]\s*([A-G](?:#|b)?)/i);
			if(m)key = m[1];

			// 演奏キー/移調後キー/プレイ(保険)
			m = seg.match(/演奏キー[::]\s*([A-G](?:#|b)?)/i);
			if(m)play = m[1];
			m = seg.match(/移調後(?:の)?キー[::]\s*([A-G](?:#|b)?)/i);
			if(m)play = m[1];
			m = seg.match(/プレイ[::]\s*([A-G](?:#|b)?)/i);
			if(m)play = m[1];
		}
		return play || key || orig || null;
	}

	function extractKeyFromParagraph(el){
		const txt = el.innerText || el.textContent || "";
		return extractEffectiveKey(txt);
	}

	// lineEl内のspan.chordを処理
	// mode: "deg"(度数へ)/ "orig"(元へ)
	function processLine(lineEl,currentKey,mode){
		if(!lineEl)return;
		const romanMap = currentKey ? buildRomanMap(currentKey) : null;
		const keyAcc = currentKey ? buildKeyAccidentalMap(currentKey) : null;
		lineEl.querySelectorAll("span.chord").forEach((el)=>{
			const textNow = el.innerText || el.textContent || "";
			if(!el.dataset.originalChord){
				el.dataset.originalChord = textNow;
			}
			if(mode === "deg"){
				const source = el.dataset.originalChord;
				if(!source)return;
				if(source === "N.C."){
					el.innerText = source;
					el.dataset.degreeChord = source;
					return;
				}
				if(!romanMap || !keyAcc){
					el.innerText = source;
					return;
				}
				const converted = convertChordSymbol(source,romanMap,keyAcc);
				el.innerText = converted;
				el.dataset.degreeChord = converted;
			}else if(mode === "orig"){
				if(el.dataset.originalChord){
					el.innerText = el.dataset.originalChord;
				}
			}
		});
	}

	// 文書全体(Key/Original Key/Play のたびに currentKey を更新)
	function convertDocument(mode = "deg"){
		let currentKey = null;
		const nodes = [...document.querySelectorAll("p.key, p.line")];
		for(let i = 0;i < nodes.length;i++){
			const el = nodes[i];
			if(el.matches("p.key")){
				const k = extractKeyFromParagraph(el);
				if(k)currentKey = k;
				continue;
			}
			if(el.matches("p.line")){
				processLine(el,currentKey,mode);
			}
		}
	}

	// Play: が書き換わったら自動で再変換(度数表示中のみ)
	function hookPlayKeyObserver(){
		const target = document.querySelector('p.key') || document.body;
		if(!target)return;
		let timer = null;
		const obs = new MutationObserver((muts)=>{
			let touched = false;
			for(const m of muts){
				const node = m.target;
				if(!node)continue;
				const container = (node.nodeType === 3 ? node.parentNode : node);
				if(container?.closest && container.closest('p.key')){
					touched = true;break;
				}
			}
			if(touched && isDeg){
				if(timer)clearTimeout(timer);
				timer = setTimeout(()=>{convertDocument("deg");},120);
			}
		});
		obs.observe(document.body,{subtree:true,childList:true,characterData:true});
	}

	function update(){
		if(updating)return;
		updating = true;
		main();
		setTimeout(()=>{updating = false;},600);
	}

	function locationChange(targetPlace = document){
		const observer = new MutationObserver(mutations=>{
			if(currentUrl !== document.location.href){
				currentUrl = document.location.href;
				try{
					update();
				}catch(error){console.error(error)}
			}
		});
		const config = {childList:true,subtree:true};
		observer.observe(targetPlace,config);
	}

	function sleep(time){
		return new Promise((resolve)=>{
			setTimeout(()=>{return resolve(time)},time);
		});
	}

	function decodeHtml(html){
		const txt = document.createElement("div");
		txt.innerHTML = html;
		return txt.textContent;
	}

	function h(tag,props = {},...children){
		const el = document.createElement(tag);
		for(const key in props){
			const val = props[key];
			if(key === "style" && typeof val === "object"){
				Object.assign(el.style,val);
			}else if(key.startsWith("on") && typeof val === "function"){
				el.addEventListener(key.slice(2).toLowerCase(),val);
			}else if(key.startsWith("aria-") || key === "role"){
				el.setAttribute(key,val);
			}else if(key === "dataset" && typeof val === "object"){
				for(const dataKey in val){
					if(val[dataKey] != null){
						el.dataset[dataKey] = val[dataKey];
					}
				}
			}else if(key.startsWith("data-")){
				const prop = key.slice(5).replace(/-([a-z])/g,(_,c)=>c.toUpperCase());
				el.dataset[prop] = val;
			}else if(key === "ref" && typeof val === "function"){
				val(el);
			}else if(key in el){
				el[key] = val;
			}else{
				el.setAttribute(key,val);
			}
		}
		for(let i = 0;i < children.length;i++){
			const child = children[i];
			if(Array.isArray(child)){
				for(const nested of child){
					if(nested == null || nested === false)continue;
					el.appendChild(typeof nested === "string" || typeof nested === "number" ? document.createTextNode(nested) : nested);
				}
			}else if(child != null && child !== false){
				el.appendChild(typeof child === "string" || typeof child === "number" ? document.createTextNode(child) : child);
			}
		}
		return el;
	}

	function waitElementAndGet({query,searchFunction = 'querySelector',interval = 100,retry = 25,searchPlace = document,faildToThrow = false} = {}){
		if(!query)throw(`query is needed`);
		return new Promise((resolve,reject)=>{
			const MAX_RETRY_COUNT = retry;
			let retryCounter = 0;
			let searchFn;
			switch(searchFunction){
				case'querySelector':
					searchFn = ()=>searchPlace.querySelector(query);
					break;
				case'getElementById':
					searchFn = ()=>searchPlace.getElementById(query);
					break;
				case'XPath':
					searchFn = ()=>{
						let section = document.evaluate(query,searchPlace,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue;
						return section;
					};
					break;
				case'XPathAll':
					searchFn = ()=>{
						let sections = document.evaluate(query,searchPlace,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);
						let result = [];
						for(let i = 0;i < sections.snapshotLength;i++){
							result.push(sections.snapshotItem(i));
						}
						if(result.length >= 1)return result;
					};
					break;
				default:
					searchFn = ()=>searchPlace.querySelectorAll(query);
			}
			const setIntervalId = setInterval(findTargetElement,interval);
			function findTargetElement(){
				retryCounter++;
				if(retryCounter > MAX_RETRY_COUNT){
					clearInterval(setIntervalId);
					if(faildToThrow){
						return reject(`Max retry count (${MAX_RETRY_COUNT}) reached for query: ${query}`);
					}else{
						console.warn(`Max retry count (${MAX_RETRY_COUNT}) reached for query: ${query}`);
						return resolve(null);
					}
				}
				let targetElements = searchFn();
				if(targetElements && (!(targetElements instanceof NodeList) || targetElements.length >= 1)){
					clearInterval(setIntervalId);
					return resolve(targetElements);
				}
			}
		});
	}

	locationChange();
	main();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址