您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
ja.chordwiki.orgのキーが明記されいるページのコード名をディグリーに変換
当前为
// ==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或关注我们的公众号极客氢云获取最新地址