您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
ja.chordwiki.orgのキーが明記されいるページのコード名をディグリーに変換(キー未表記ページは推定)
当前为
// ==UserScript== // @name [chordwiki] コード to ディグリー // @description ja.chordwiki.orgのキーが明記されいるページのコード名をディグリーに変換(キー未表記ページは推定) // @namespace https://gf.qytechs.cn/ja/users/1023652 // @version 1.0.0.1 // @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; // 転調判定のしきい値 const MOD_MIN_FIRST = 1.5; // ブロック内ベストキーの最低スコア const MOD_CHANGE_DELTA = 1.2; // 直前キーとの差がこの値以上で転調 const MIN_TOKENS_FOR_INFER = 7; let isDeg = false; async function main(){ addConvertFab(); setupLineKeyUI(); // キー未表記ページ: ブロック推定→行ドロップダウン初期化 attachLineKeyHandlers(); // 変更伝播(「-」継承の再計算) hookPlayKeyObserver(); // Play: 変更検出(グローバルキーありページ) applyResponsiveLayout(); } // ===== 右下固定の丸ボタン(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') || !!document.querySelector('select.cw-line-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; } // ===== 音名↔半音、キー候補、キー分解 ===== const NOTE_TO_PC = {"C":0,"B#":0,"C#":1,"Db":1,"D":2,"D#":3,"Eb":3,"E":4,"Fb":4,"E#":5,"F":5,"F#":6,"Gb":6,"G":7,"G#":8,"Ab":8,"A":9,"A#":10,"Bb":10,"B":11,"Cb":11}; const PC_TO_SHARP = ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"]; const PC_TO_FLAT = ["C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","B"]; function nameToPc(name){ const n = (name || "").trim(); return NOTE_TO_PC[n] != null ? NOTE_TO_PC[n] : null; } function pcToName(pc,prefer = "either"){ pc = ((pc % 12) + 12) % 12; if(prefer === "flat")return PC_TO_FLAT[pc]; if(prefer === "sharp")return PC_TO_SHARP[pc]; return PC_TO_SHARP[pc]; } const KEY_CAND_MAJOR = ["C","G","D","A","E","B","F#","C#","F","Bb","Eb","Ab","Db","Gb","Cb"]; const KEY_CAND_MINOR = ["Am","Em","Bm","F#m","C#m","G#m","D#m","A#m","Dm","Gm","Cm","Fm","Bbm","Ebm","Abm"]; function splitKeyName(key){ const m = (key || "").trim().match(/^([A-G])([#bB]?)(m)?$/i); if(!m)return {tonic:"C",isMinor:false,accPrefer:"either"}; const letter = m[1].toUpperCase(); const acc = (m[2] || "") === "B" ? "b" : (m[2] || ""); const isMinor = !!m[3]; const accPrefer = acc === "b" ? "flat" : (acc === "#" ? "sharp" : "either"); return {tonic:letter + acc,isMinor,accPrefer}; } function relativeMajorName(minorKey){ const s = splitKeyName(minorKey); if(!s.isMinor)return s.tonic; const pcMin = nameToPc(s.tonic); const pcMaj = (pcMin + 3) % 12; // +3半音で相対メジャー return pcToName(pcMaj,s.accPrefer); } // ===== キーの調号マップ(メジャー/マイナー対応) ===== function buildKeyAccidentalMap(key){ const s = splitKeyName(key); const k = s.isMinor ? relativeMajorName(key) : s.tonic; 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" "/C" "(F#m7)" "(/C)" など function convertChordSymbol(sym,romanMap,keyAcc){ const s = (sym || "").trim(); if(s === "N.C.")return s; // 1) 先に「( … ) / ( … )」で丸ごと包まれている形を処理 const mWrap = s.match(/^([(\(])\s*(.+?)\s*([)\)])$/); if(mWrap){ const left = mWrap[1], inner = mWrap[2], right = mWrap[3]; if(/\s/.test(inner)){ const parts = inner.split(/\s+/).filter(Boolean).map(t=>convertChordSymbol(t,romanMap,keyAcc)); return left + parts.join(" ") + right; } return left + convertChordSymbol(inner,romanMap,keyAcc) + right; } // 2) ベースのみ "/C" など 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; } // 3) 通常パターン 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; m = seg.match(/Play[::]\s*([A-G](?:#|b)?)/i); if(m)play = m[1]; 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]; 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 parseChordSymbolBasic(sym){ let s = (sym || "").trim(); if(!s || s === "N.C.")return null; const wrap = s.match(/^([(\(])\s*(.+?)\s*([)\)])$/); if(wrap)s = wrap[2]; let m = s.match(/^\/\s*([A-G](?:#|b)?)\s*$/i); if(m)return {root:null,bass:m[1].toUpperCase(),quality:null,isDom7:false,isHalfDim:false}; m = s.match(/^([A-G](?:#|b)?)(.*?)(?:\/([A-G](?:#|b)?))?$/i); if(!m)return null; const root = m[1].toUpperCase(); const q = (m[2] || ""); const bass = m[3] ? m[3].toUpperCase() : null; let quality = "maj"; if(/(dim|°|o)/i.test(q))quality = "dim"; else if(/aug|\+/i.test(q))quality = "aug"; else if(/m(?!aj)/.test(q))quality = "min"; else quality = "maj"; const isDom7 = /(^|[^a-z])7(?!-?5)/i.test(q); const isHalfDim = /m7-5|ø/i.test(q); return {root,bass,quality,isDom7,isHalfDim}; } const EXPECT_MAJOR = {1:"maj",2:"min",3:"min",4:"maj",5:"maj",6:"min",7:"dim"}; const EXPECT_MINOR = {1:"min",2:"dim",3:"maj",4:"min",5:"min",6:"maj",7:"maj"}; function degreeParts(note,romanMap,keyAcc){ const d = noteToDegree(note,romanMap,keyAcc); const m = d.match(/^([ⅠⅡⅢⅣⅤⅥⅦ])([#b]{1,2})?$/); if(!m)return {roman:null,accs:""}; return {roman:m[1],accs:m[2] || ""}; } function romanToIndex(r){ return {"Ⅰ":1,"Ⅱ":2,"Ⅲ":3,"Ⅳ":4,"Ⅴ":5,"Ⅵ":6,"Ⅶ":7}[r] || null; } function scoreChordForKey(ch,keyName){ const s = splitKeyName(keyName); const romanMap = buildRomanMap(s.tonic); const keyAcc = buildKeyAccidentalMap(keyName); let score = 0; if(ch.root){ const dp = degreeParts(ch.root,romanMap,keyAcc); if(dp.roman){ const diatonic = dp.accs === ""; if(diatonic)score += 2; else score -= dp.accs.length; const idx = romanToIndex(dp.roman); //const expect = s.isMinor ? EXPECT_MINOR[idx] : EXPECT_MAJOR[idx]; const expect = EXPECT_MAJOR[idx]; if(ch.quality === "dim"){ score += expect === "dim" ? 1 : -0.5; }else if(ch.quality === "min"){ score += expect === "min" ? 1 : -0.25; }else if(ch.quality === "maj"){ if(expect === "maj")score += 1; else if(s.isMinor && idx === 5)score += 0.8; else score -= 0.25; }else{ score -= 0.1; } if(ch.isDom7 && idx === 5)score += 0.5; if(ch.isHalfDim && idx === 7)score += 0.5; } } if(ch.bass){ const dpb = degreeParts(ch.bass,romanMap,keyAcc); if(dpb.roman){ score += dpb.accs === "" ? 0.25 : -0.25; } } return score; } function scoreTokensForKey(tokens,keyName){ let s = 0; for(let i = 0;i < tokens.length;i++)s += scoreChordForKey(tokens[i],keyName); return s; } function bestKeyAndScore(tokens){ //const cand = [...KEY_CAND_MAJOR,...KEY_CAND_MINOR]; const cand = KEY_CAND_MAJOR; let bestKey = "C", bestScore = Number.NEGATIVE_INFINITY; for(let i = 0;i < cand.length;i++){ const k = cand[i]; const sc = scoreTokensForKey(tokens,k); if(sc > bestScore){bestScore = sc; bestKey = k;} } return {bestKey,bestScore}; } function inferKeyFromChordArray(chords){ const toks = chords.map(parseChordSymbolBasic).filter(Boolean); if(!toks.length)return "C"; const {bestKey,bestScore} = bestKeyAndScore(toks); return bestScore < -1 ? "C" : bestKey; } // ===== ブロック構築(div[oncopy]/div[onCopy] 内を優先) ===== function buildLineBlocks(){ const container = document.querySelector('div[oncopy], div[onCopy]') || document; // p.line と br をドキュメント順に走査し、次のいずれかでブロックを区切る: // - <br> // - p.line 以外(例: p.line.comment など)は「境界」として扱う const seq = Array.from(container.querySelectorAll('p.line, br')); const blocks = []; let cur = []; for(let i = 0;i < seq.length;i++){ const el = seq[i]; if(el.tagName === 'BR'){ if(cur.length){blocks.push(cur); cur = [];} continue; } // p.line 系 if(el.tagName === 'P'){ if(el.className === 'line'){ cur.push(el); }else{ // line comment などに遭遇 → ここで区切る if(cur.length){blocks.push(cur); cur = [];} } } } if(cur.length)blocks.push(cur); // 念のため、line を1つ以上含むもののみ返す return blocks.filter(b=>b.some(l=>l.className === 'line')); } function collectTokensFromLines(lines){ const tokens = []; for(let i = 0;i < lines.length;i++){ const line = lines[i]; const spans = line.querySelectorAll('span.chord'); for(let j = 0;j < spans.length;j++){ const raw = (spans[j].dataset.originalChord || spans[j].textContent || "").trim(); const tok = parseChordSymbolBasic(raw); if(tok)tokens.push(tok); } } return tokens; } // ===== 行ドロップダウン(「-」=継承) ===== const KEY_OPTIONS = [ "C","G","D","A","E","B","F#","C#","F","Bb","Eb","Ab","Db","Gb","Cb", //"Am","Em","Bm","F#m","C#m","G#m","D#m","A#m","Dm","Gm","Cm","Fm","Bbm","Ebm","Abm" ]; function createLineKeySelect(selectedValue = "-"){ const sel = document.createElement("select"); sel.className = "cw-line-key"; sel.title = "この行のキー(推定/継承)"; { const opt = document.createElement("option"); opt.value = "-"; opt.textContent = "-"; sel.appendChild(opt); } for(let i = 0;i < KEY_OPTIONS.length;i++){ const k = KEY_OPTIONS[i]; const opt = document.createElement("option"); opt.value = k; opt.textContent = k; sel.appendChild(opt); } sel.value = selectedValue || "-"; Object.assign(sel.style,{ position: "absolute", fontSize: "11px", opacity: "0.85", zIndex: "2147483647" }); if(isMobileView()){ sel.style.left = "5px"; sel.style.top = "-30px"; sel.style.background = "#f3f4f6"; sel.style.border = "1px solid #d1d5db"; }else{ sel.style.left = "-50px"; sel.style.top = "-16px"; } sel.addEventListener("mouseenter",()=>{sel.style.opacity = "1";}); sel.addEventListener("mouseleave",()=>{sel.style.opacity = "0.85";}); return sel; } function applyResponsiveLayout(){ const mobile = isMobileView(); const lines = document.querySelectorAll("p.line"); for(let i = 0;i < lines.length;i++){ const line = lines[i]; const sel = line.querySelector(":scope > select.cw-line-key"); // セレクトがあれば位置を再適用(画面回転・リサイズに対応) if(sel){ if(mobile){ sel.style.left = "5px"; sel.style.top = "-30px"; sel.style.background = "#f3f4f6"; sel.style.border = "1px solid #d1d5db"; }else{ sel.style.left = "-50px"; sel.style.top = "-16px"; } } // 行の上側に余白を付けて、ドロップダウンが「行と行の間」に来るようにする if(mobile){ if(!line.dataset.cwMobileSpaced){ // 既存のinline marginTopを退避してから上書き line.dataset.cwPrevMarginTop = line.style.marginTop || ""; line.style.marginTop = "32px"; // ドロップダウンの -30px を収める余白 line.dataset.cwMobileSpaced = "1"; } }else{ // PCに戻ったら元のmarginに復帰 if(line.dataset.cwMobileSpaced){ line.style.marginTop = line.dataset.cwPrevMarginTop || ""; delete line.dataset.cwMobileSpaced; delete line.dataset.cwPrevMarginTop; } } } } // ブロック推定→行セレクト生成&初期化(1ブロックの先頭行のみ明示キー、残りは「-」) function setupLineKeyUI(){ // グローバル <p class="key"> がある場合は作らない if(document.querySelector("p.key"))return; const blocks = buildLineBlocks(); if(!blocks.length)return; // まず全対象行に select を装着 for(let b = 0;b < blocks.length;b++){ const lines = blocks[b]; for(let i = 0;i < lines.length;i++){ const line = lines[i]; if(line.className !== "line")continue; if(!line.querySelector(":scope > select.cw-line-key")){ const cs = window.getComputedStyle(line); if(cs.position === "static")line.style.position = "relative"; const sel = createLineKeySelect("-"); line.insertBefore(sel,line.firstChild); } } } // ブロック単位で推定 & 転調判定 → 初期シード seedKeysByBlocks(blocks); applyResponsiveLayout(); } function seedKeysByBlocks(blocks){ let prevEffective = null; for(let b = 0;b < blocks.length;b++){ const lines = blocks[b]; // このブロックのトークンを取得 let tokens = collectTokensFromLines(lines); // ★最初のブロックでコード数が少なければ、次ブロック以降を順に結合して閾値を満たすまで拡張 if(b === 0 && tokens.length < MIN_TOKENS_FOR_INFER){ let nb = b + 1; while(nb < blocks.length && tokens.length < MIN_TOKENS_FOR_INFER){ tokens = tokens.concat(collectTokensFromLines(blocks[nb])); nb++; } } let explicitKey = null; // ★コード数が閾値未満なら推定しない(= 継承) if(tokens.length >= MIN_TOKENS_FOR_INFER){ if(b === 0){ // 先頭ブロック:結合結果で推定(既存ルール) const {bestKey} = bestKeyAndScore(tokens); explicitKey = bestKey || "C"; }else{ // 以降のブロック:転調検出も既存ルール+コード数閾値を満たすときのみ const {bestKey,bestScore} = bestKeyAndScore(tokens); const prevScore = tokens.length ? scoreTokensForKey(tokens,prevEffective) : 0; if(bestScore >= MOD_MIN_FIRST && bestKey !== prevEffective && bestScore >= prevScore + MOD_CHANGE_DELTA){ explicitKey = bestKey; } } } // セレクトと effectiveKey を反映 for(let i = 0;i < lines.length;i++){ const line = lines[i]; if(line.className !== "line")continue; const sel = line.querySelector(":scope > select.cw-line-key"); if(i === 0){ if(explicitKey){ sel.value = explicitKey; line.dataset.effectiveKey = explicitKey; prevEffective = explicitKey; }else{ // 推定無し → 継承(prevEffective が無ければ空) sel.value = "-"; line.dataset.effectiveKey = prevEffective || ""; } }else{ sel.value = "-"; line.dataset.effectiveKey = prevEffective || ""; } } } } // セレクト変更→「-」継承を後続へ伝播 function attachLineKeyHandlers(){ if(document.querySelector("p.key"))return; // グローバルキーがある場合は不要 const lines = [...document.querySelectorAll("p.line")].filter(l=>l.className === "line"); for(let i = 0;i < lines.length;i++){ const line = lines[i]; const sel = line.querySelector(":scope > select.cw-line-key"); if(!sel)continue; sel.addEventListener("change",()=>{ recomputeEffectiveKeysFrom(lines,line); if(isDeg){ for(let j = 0;j < lines.length;j++){ processLine(lines[j],lines[j].dataset.effectiveKey || null,"deg"); } } }); } } function recomputeEffectiveKeysFrom(lines,startLine){ let lastExplicit = null; for(let i = 0;i < lines.length;i++){ const line = lines[i]; const sel = line.querySelector(":scope > select.cw-line-key"); const v = sel ? sel.value : "-"; if(line === startLine)break; if(v && v !== "-"){ lastExplicit = v; }else{ if(line.dataset.effectiveKey){ lastExplicit = line.dataset.effectiveKey; } } } let prev = lastExplicit; let startIdx = lines.indexOf(startLine); if(startIdx < 0)startIdx = 0; for(let i = startIdx;i < lines.length;i++){ const line = lines[i]; const sel = line.querySelector(":scope > select.cw-line-key"); const v = sel ? sel.value : "-"; if(v && v !== "-"){ prev = v; line.dataset.effectiveKey = prev; }else{ line.dataset.effectiveKey = prev || ""; } } } // ===== 既存ライン処理 ===== function extractKeyFromParagraph(el){ const txt = el.innerText || el.textContent || ""; return extractEffectiveKey(txt); } // 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 or 行単位effectiveKey)で変換 function convertDocument(mode = "deg"){ const hasGlobalKey = !!document.querySelector("p.key"); let currentKey = null; if(!hasGlobalKey){ const lines = [...document.querySelectorAll("p.line")].filter(l=>l.className === "line"); if(lines.length){ if(!lines[0].querySelector(":scope > select.cw-line-key")){ setupLineKeyUI(); attachLineKeyHandlers(); } recomputeEffectiveKeysFrom(lines,lines[0]); } } 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")){ const line = el; let lineKey = null; if(hasGlobalKey){ lineKey = currentKey; }else{ lineKey = line.dataset.effectiveKey || null; } processLine(line,lineKey,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 isMobileView(){ return (window.matchMedia && window.matchMedia("(max-width: 768px)").matches) || /Android|iPhone|iPod|Windows Phone|Mobile/i.test(userAgent); } 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或关注我们的公众号极客氢云获取最新地址