1/** 2 * vocab_renderer.js 3 * Shared vocabulary table rendering utilities for the main app and PDF editor. 4 * 5 * Exposes window.VocabRenderer with: 6 * - filterVocabByThreshold(data, threshold) 7 * - renderVocabTable(containerEl, data, options) 8 * - buildAnkiTsv(data) 9 * - exportAnkiTsv(data, filename) 10 * 11 * Expected VocabItem schema for renderVocabTable/buildAnkiTsv: 12 * { word, reading, originalWord?, context?, jlptLevel, posTag, frequency, order, 13 * isAmbiguous?, altReadings?, hasDefinitions? } 14 */ 15(function (global) { 16 'use strict'; 17 18 function t(key, fallback) { 19 var value = global.AppConfig && global.AppConfig.i18n && global.AppConfig.i18n[key]; 20 return typeof value === 'string' && value.length > 0 ? value : fallback; 21 } 22 23 function filterVocabByThreshold(data, threshold) { 24 var t = Math.round(parseFloat(threshold)); 25 return data.filter(function (item) { 26 return item.jlptLevel === 0 || item.jlptLevel <= t; 27 }); 28 } 29 30 function renderVocabTable(containerEl, data, options) { 31 var opts = Object.assign({ 32 readingVisible: true, 33 furiganaHideOnHover: false, 34 activeSortBy: '', 35 translatePosFn: function (x) { return x; }, 36 flagBtnTitle: t('reportError', 'Report error'), 37 definitionsBtnTitle: t('showDefinitions', 'Show definitions') 38 }, options); 39 40 containerEl.innerHTML = ''; 41 42 if (data.length === 0) { 43 var emptyRow = document.createElement('tr'); 44 var emptyTd = document.createElement('td'); 45 emptyTd.colSpan = 3; 46 emptyTd.className = 'vocabulary-ledger-empty'; 47 emptyTd.textContent = t('noVocabData', 'No vocabulary data available'); 48 emptyRow.appendChild(emptyTd); 49 containerEl.appendChild(emptyRow); 50 return; 51 } 52 53 data.forEach(function (item) { 54 var word = item.word; 55 var reading = item.reading; 56 var jlptLevel = item.jlptLevel || 0; 57 var posTag = item.posTag || ''; 58 59 var row = document.createElement('tr'); 60 row.className = 'vocabulary-ledger-row'; 61 if (opts.furiganaHideOnHover) { row.className += ' hide-on-hover'; } 62 63 // --- First cell: word + badges --- 64 var td1 = document.createElement('td'); 65 td1.className = 'vocabulary-ledger-term'; 66 td1.dataset.surface = word; 67 td1.dataset.dictForm = word; 68 td1.dataset.reading = reading; 69 td1.dataset.jlpt = String(jlptLevel); 70 td1.dataset.pos = posTag; 71 td1.dataset.altReadings = item.altReadings || ''; 72 td1.dataset.ambiguous = String(!!item.isAmbiguous); 73 74 var wordSpan = document.createElement('span'); 75 wordSpan.className = 'vocabulary-ledger-word'; 76 wordSpan.dataset.word = word; 77 wordSpan.dataset.dictForm = word; 78 wordSpan.dataset.reading = reading; 79 wordSpan.textContent = word; 80 td1.appendChild(wordSpan); 81 82 // JLPT badge 83 if (jlptLevel > 0) { 84 var jlptBadge = document.createElement('span'); 85 jlptBadge.className = 'vocabulary-ledger-tag'; 86 jlptBadge.textContent = 'N' + jlptLevel; 87 td1.appendChild(jlptBadge); 88 } 89 90 // POS badge 91 if (posTag) { 92 var posBadge = document.createElement('span'); 93 posBadge.className = 'vocabulary-ledger-tag'; 94 posBadge.textContent = opts.translatePosFn(posTag); 95 td1.appendChild(posBadge); 96 } 97 98 // Frequency badge 99 if (opts.activeSortBy === 'frequency' && item.frequency > 1) { 100 var freqBadge = document.createElement('span'); 101 freqBadge.className = 'vocabulary-ledger-tag'; 102 freqBadge.textContent = '\u00d7' + item.frequency; 103 td1.appendChild(freqBadge); 104 } 105 106 row.appendChild(td1); 107 108 // --- Second cell: reading --- 109 var td2 = document.createElement('td'); 110 td2.className = 'vocabulary-ledger-reading-cell'; 111 112 var readingSpan = document.createElement('span'); 113 readingSpan.className = opts.readingVisible 114 ? 'vocabulary-ledger-reading' 115 : 'vocabulary-ledger-reading reading-masked px-2 py-0.5'; 116 readingSpan.textContent = reading; 117 118 // Help button for ambiguous words with definitions 119 if (item.isAmbiguous && item.altReadings) { 120 var altList = item.altReadings.split(','); 121 readingSpan.textContent = reading + '\u3001' + altList.join('\u3001'); 122 if (item.hasDefinitions) { 123 var helpBtn = document.createElement('button'); 124 helpBtn.className = 'definition-help-btn ml-2'; 125 helpBtn.dataset.word = word; 126 helpBtn.title = opts.definitionsBtnTitle; 127 helpBtn.textContent = '?'; 128 readingSpan.appendChild(helpBtn); 129 } 130 } 131 132 td2.appendChild(readingSpan); 133 row.appendChild(td2); 134 135 // --- Third cell: flag button --- 136 var td3 = document.createElement('td'); 137 td3.className = 'vocabulary-ledger-action-cell'; 138 139 var flagBtn = document.createElement('button'); 140 flagBtn.className = 'vocabulary-ledger-report flag-btn error-report-btn'; 141 flagBtn.dataset.word = word; 142 flagBtn.dataset.reading = reading; 143 flagBtn.title = opts.flagBtnTitle; 144 145 var svgNs = 'http://www.w3.org/2000/svg'; 146 var svg = document.createElementNS(svgNs, 'svg'); 147 svg.classList.add('w-5', 'h-5'); 148 svg.setAttribute('fill', 'none'); 149 svg.setAttribute('stroke', 'currentColor'); 150 svg.setAttribute('stroke-width', '2'); 151 svg.setAttribute('stroke-linecap', 'round'); 152 svg.setAttribute('stroke-linejoin', 'round'); 153 svg.setAttribute('viewBox', '0 0 24 24'); 154 155 var path1 = document.createElementNS(svgNs, 'path'); 156 path1.setAttribute('d', 'M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z'); 157 svg.appendChild(path1); 158 159 var line1 = document.createElementNS(svgNs, 'line'); 160 line1.setAttribute('x1', '4'); 161 line1.setAttribute('y1', '22'); 162 line1.setAttribute('x2', '4'); 163 line1.setAttribute('y2', '15'); 164 svg.appendChild(line1); 165 166 flagBtn.appendChild(svg); 167 td3.appendChild(flagBtn); 168 row.appendChild(td3); 169 170 containerEl.appendChild(row); 171 }); 172 } 173 174 function normalizeAnkiTsvField(value) { 175 return String(value == null ? '' : value) 176 .replace(/[\t\r\n]+/g, ' ') 177 .trim(); 178 } 179 180 function buildAnkiTsv(data) { 181 var headers = [ 182 '#separator:Tab', 183 '#html:false', 184 '#tags:ezfurigana', 185 '#columns:Expression\tReading\tSurface\tContext\tJLPT\tPart of Speech\tFrequency\tAlternative Readings' 186 ]; 187 var rows = data.map(function (item) { 188 return [ 189 item.word, 190 item.reading, 191 item.originalWord || item.word, 192 item.context || '', 193 item.jlptLevel ? 'N' + item.jlptLevel : '', 194 item.posTag || '', 195 item.frequency || 1, 196 item.altReadings || '' 197 ].map(normalizeAnkiTsvField).join('\t'); 198 }); 199 return headers.concat(rows).join('\n'); 200 } 201 202 function exportAnkiTsv(data, filename) { 203 var blob = new Blob(['\ufeff' + buildAnkiTsv(data)], { 204 type: 'text/tab-separated-values;charset=utf-8' 205 }); 206 var url = URL.createObjectURL(blob); 207 var a = document.createElement('a');
208 Object.assign(a, { href: url, download: filename }); 209 document.body.appendChild(a); 210 a.click(); 211 document.body.removeChild(a); 212 URL.revokeObjectURL(url); 213 } 214 215 global.VocabRenderer = { 216 filterVocabByThreshold: filterVocabByThreshold, 217 renderVocabTable: renderVocabTable, 218 buildAnkiTsv: buildAnkiTsv, 219 exportAnkiTsv: exportAnkiTsv 220 }; 221 222}(window));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.