Last active
September 29, 2025 16:20
-
-
Save piazzatron/309b533dfae9a844eb9d85e7cd88519d to your computer and use it in GitHub Desktop.
Print Wani Vocab By Lesson Level
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // INSTRUCTIONS | |
| // 1) Ensure Anki is open + AnkiConnect is installed, and your origin (e.g. https://www.wanikani.com) is whitelisted. | |
| // 2) (Optional) Set an ignore list in localStorage: | |
| // localStorage.setItem('WK_IGNORE_LIST', JSON.stringify(['工事', '猫'])); | |
| // 3) Open WaniKani Advanced Lesson Picker, paste this in the console. | |
| (async () => { | |
| const ANKI_URL = 'http://127.0.0.1:8765'; | |
| const FIELD_NAME = 'Expression'; | |
| const IGNORE_LS_KEY = 'WK_IGNORE_LIST'; | |
| // ---- helpers ---- | |
| const stripHtml = (s) => String(s ?? '').replace(/<[^>]*>/g, '').replace(/ /g, ' ').trim(); | |
| const nfc = (s) => (s ?? '').normalize('NFC').trim(); | |
| const esc = (s) => String(s).replace(/"/g, '\\"'); | |
| function loadIgnoreSet() { | |
| const raw = localStorage.getItem(IGNORE_LS_KEY); | |
| if (!raw) return new Set(); | |
| try { | |
| const parsed = JSON.parse(raw); | |
| const arr = Array.isArray(parsed) ? parsed | |
| : (parsed && Array.isArray(parsed.words)) ? parsed.words | |
| : []; | |
| return new Set(arr.map(nfc)); | |
| } catch { | |
| const arr = String(raw).split(/[,\n\r\t ]+/).map(s => s.trim()).filter(Boolean); | |
| return new Set(arr.map(nfc)); | |
| } | |
| } | |
| async function ankiInvoke(action, params = {}) { | |
| const res = await fetch(ANKI_URL, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ action, version: 6, params }), | |
| }); | |
| const json = await res.json(); | |
| if (json.error) throw new Error(json.error); | |
| return json.result; | |
| } | |
| // Returns array parallel to queries: [[ids], [ids], ...] (AnkiConnect multi → array-of-arrays) | |
| async function ankiMultiFindNotes(queries) { | |
| const actions = queries.map(q => ({ action: 'findNotes', params: { query: q } })); | |
| const res = await fetch(ANKI_URL, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ action: 'multi', version: 6, params: { actions } }), | |
| }); | |
| const json = await res.json(); | |
| if (json.error) throw new Error(json.error); | |
| if (Array.isArray(json.result) && (json.result.length === 0 || Array.isArray(json.result[0]))) { | |
| return json.result; | |
| } | |
| // fallback to single calls | |
| const out = []; | |
| for (const q of queries) { | |
| try { out.push(await ankiInvoke('findNotes', { query: q }) || []); } | |
| catch { out.push([]); } | |
| } | |
| return out; | |
| } | |
| async function wordsPresentInAnki(words) { | |
| if (!words.length) return new Set(); | |
| const queries = words.map(w => `${FIELD_NAME}:"${esc(w)}"`); // Expression:"猫" | |
| const perWordIds = await ankiMultiFindNotes(queries); | |
| const allIds = [...new Set(perWordIds.flat())]; | |
| if (!allIds.length) return new Set(); | |
| const notes = await ankiInvoke('notesInfo', { notes: allIds }); | |
| const byId = new Map(notes.map(n => [n.noteId, n])); | |
| const inAnki = new Set(); | |
| for (let i = 0; i < words.length; i++) { | |
| const w = nfc(words[i]); | |
| const ids = perWordIds[i] || []; | |
| for (const id of ids) { | |
| const n = byId.get(id); | |
| const val = nfc(stripHtml(n?.fields?.[FIELD_NAME]?.value ?? '')); | |
| if (val === w) { inAnki.add(w); break; } | |
| } | |
| } | |
| return inAnki; | |
| } | |
| // ---- fetch JLPT CSV ---- | |
| const res = await fetch('https://raw.githubusercontent.com/elzup/jlpt-word-list/master/out/all.csv'); | |
| const text = await res.text(); | |
| const lines = text.split('\n').slice(1); | |
| const jlptMap = {}; | |
| const meanings = {}; | |
| const readings = {}; | |
| for (const line of lines) { | |
| if (!line.trim()) continue; | |
| const cols = line.split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/); | |
| const expr = (cols[0] || '').replace(/^"|"$/g, ''); | |
| const read = (cols[1] || '').replace(/^"|"$/g, ''); | |
| const mean = (cols[2] || '').replace(/^"|"$/g, ''); | |
| const tags = (cols[3] || '').replace(/^"|"$/g, ''); | |
| const m = tags.match(/JLPT_.*(\d)/); | |
| if (expr && m?.[1]) { | |
| const level = 'N' + m[1]; | |
| jlptMap[expr] = level; | |
| readings[expr] = read; | |
| meanings[expr] = mean; | |
| } | |
| } | |
| // ---- scrape WaniKani panels, collect words and the elements to hide ---- | |
| const waniLevelMap = {}; | |
| const wordToElements = new Map(); // word → [tileElements] | |
| document.querySelectorAll('.lesson-picker__level').forEach(section => { | |
| const level = section.querySelector('.wk-panel__title')?.textContent?.trim() || 'UNKNOWN'; | |
| const items = section.querySelectorAll('.subject-character__characters'); | |
| const words = []; | |
| items.forEach(el => { | |
| const w = nfc(el.textContent); | |
| if (!w) return; | |
| words.push(w); | |
| // choose a tile/container to hide | |
| const tile = el.closest( | |
| '.subject-character, .subject-summary, .lesson-picker__item, .subject-item, .character-grid__cell, .subject-tile' | |
| ) || el.parentElement; | |
| if (!wordToElements.has(w)) wordToElements.set(w, []); | |
| wordToElements.get(w).push(tile); | |
| }); | |
| waniLevelMap[level] = words; | |
| }); | |
| // ---- ignore list ---- | |
| const ignoreSet = loadIgnoreSet(); | |
| // ---- Anki check on all visible, non-ignored words that have JLPT info ---- | |
| const allWordsForAnki = [ | |
| ...new Set( | |
| Object.values(waniLevelMap) | |
| .flat() | |
| .filter(w => jlptMap[w]) | |
| .filter(w => !ignoreSet.has(w)) | |
| ) | |
| ]; | |
| let inAnkiSet = null; | |
| try { inAnkiSet = await wordsPresentInAnki(allWordsForAnki); } | |
| catch { inAnkiSet = null; } | |
| // ---- build output (per-level NEW, then one global ALREADY IN ANKI) ---- | |
| const out = []; | |
| const alreadyByJlpt = {}; | |
| const alreadySeen = new Set(); | |
| for (const [waniLevel, wordsRaw] of Object.entries(waniLevelMap)) { | |
| const newByJlpt = {}; | |
| for (const w of wordsRaw) { | |
| if (!jlptMap[w]) continue; | |
| if (ignoreSet.has(w)) continue; | |
| const isInAnki = inAnkiSet && inAnkiSet.has(w); | |
| if (isInAnki) { | |
| if (!alreadySeen.has(w)) { | |
| alreadySeen.add(w); | |
| (alreadyByJlpt[jlptMap[w]] ||= []).push(w); | |
| } | |
| } else { | |
| (newByJlpt[jlptMap[w]] ||= []).push(w); | |
| } | |
| } | |
| out.push(''); | |
| out.push(`WANI LEVEL ${waniLevel}`); | |
| out.push('--------------------------------'); | |
| const keys = Object.keys(newByJlpt).sort().reverse(); | |
| if (keys.length === 0) { | |
| out.push('(No new items — everything here is already in Anki or ignored)'); | |
| } else { | |
| for (const jl of keys) { | |
| out.push(`${jl}: ${newByJlpt[jl].length}`); | |
| for (const w of newByJlpt[jl]) { | |
| out.push(` ${w} (${readings[w]}), ${meanings[w]}`); | |
| } | |
| } | |
| } | |
| } | |
| if (inAnkiSet !== null) { | |
| const jlKeys = Object.keys(alreadyByJlpt).sort().reverse(); | |
| if (jlKeys.length) { | |
| out.push(''); | |
| out.push('ALREADY IN ANKI'); | |
| out.push('--------------------------------'); | |
| for (const jl of jlKeys) { | |
| out.push(`${jl}: ${alreadyByJlpt[jl].length}`); | |
| for (const w of alreadyByJlpt[jl]) { | |
| out.push(` ${w} (${readings[w]}), ${meanings[w]}`); | |
| } | |
| } | |
| } | |
| } else { | |
| out.push(''); | |
| out.push('NOTE: Anki check failed; exported everything as NEW (ignore list still applied).'); | |
| } | |
| console.log(out.join('\n')); | |
| // ---- FINAL STEP: hide tiles for any words that are in Anki OR in ignore list ---- | |
| const toHide = new Set([ | |
| ...ignoreSet, | |
| ...(inAnkiSet ? inAnkiSet : []) | |
| ]); | |
| for (const w of toHide) { | |
| const els = wordToElements.get(w); | |
| if (!els) continue; | |
| els.forEach(el => { if (el && el.style) el.style.display = 'none'; }); | |
| } | |
| // Optional: provide an unhide helper in case you want to restore them quickly | |
| window.WK_unhideAllIgnoredOrAnki = () => { | |
| for (const w of toHide) { | |
| const els = wordToElements.get(w) || []; | |
| els.forEach(el => { if (el && el.style) el.style.display = ''; }); | |
| } | |
| }; | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment