// Renders a BÍN paradigm (FORMS entry) as organized grammar tables instead of a // flat list. Pure function: renderParadigm(entry, currentForm, lang) -> HTML. (function (global) { function esc(s) { return (s || '').replace(/[&<>]/g, c => ({ '&': '&', '<': '<', '>': '>' }[c])); } const CASES = ['NF', 'ÞF', 'ÞGF', 'EF']; const L = { en: { case: { NF: 'Nom.', ÞF: 'Acc.', ÞGF: 'Dat.', EF: 'Gen.' }, sg: 'Sg.', sgdef: 'Sg. +art.', pl: 'Pl.', pldef: 'Pl. +art.', gender: { KK: 'Masc.', KVK: 'Fem.', HK: 'Neut.' }, voice: { GM: 'Active', MM: 'Middle' }, mood: { FH: 'Indicative', VH: 'Subjunctive' }, tense: { NT: 'Present', ÞT: 'Past' }, person: { '1P': '1st', '2P': '2nd', '3P': '3rd' }, infinitive: 'Infinitive', imperative: 'Imperative', prespart: 'Present participle', pastpart: 'Past participle', supine: 'Supine', indeclinable: 'Does not inflect', deg: { FSB: 'Positive · strong', FVB: 'Positive · weak', FST: 'Positive', MST: 'Comparative', EST: 'Superlative · strong', ESB: 'Superlative · strong', EVB: 'Superlative · weak', SB: 'Strong', VB: 'Weak' } }, zh: { case: { NF: '主格', ÞF: '賓格', ÞGF: '與格', EF: '屬格' }, sg: '單數', sgdef: '單數+冠詞', pl: '複數', pldef: '複數+冠詞', gender: { KK: '陽性', KVK: '陰性', HK: '中性' }, voice: { GM: '主動', MM: '中間' }, mood: { FH: '直陳', VH: '虛擬' }, tense: { NT: '現在', ÞT: '過去' }, person: { '1P': '第一人稱', '2P': '第二人稱', '3P': '第三人稱' }, infinitive: '不定式', imperative: '命令式', prespart: '現在分詞', pastpart: '過去分詞', supine: '動名詞', indeclinable: '不變化', deg: { FSB: '原級 · 強', FVB: '原級 · 弱', FST: '原級', MST: '比較級', EST: '最高級 · 強', ESB: '最高級 · 強', EVB: '最高級 · 弱', SB: '強變化', VB: '弱變化' } } }; const norm = t => t.replace(/\d+$/, ''); // drop only trailing variant digit (keep 1P/2P/3P) const NOUN = { kk: 1, kvk: 1, hk: 1 }; // group forms by a normalized tag -> [forms] function index(forms) { const m = {}; for (const [tag, f] of forms) { const k = norm(tag); (m[k] = m[k] || []).push(f); } return m; } function cell(forms, cur) { if (!forms || !forms.length) return '·'; const hot = forms.some(f => f.toLowerCase() === cur); const html = forms.map(f => f.toLowerCase() === cur ? `${esc(f)}` : esc(f)).join(' / '); return `${html}`; } // Generic case×(columns) table. cols = [{label, key(caseCode)->tagKey}] function caseTable(m, cur, lang, cols, title) { const t = L[lang]; const present = cols.filter(c => CASES.some(cs => m[c.key(cs)])); if (!present.length) return ''; let h = title ? `
${esc(title)}
` : ''; h += '' + present.map(c => ``).join('') + ''; for (const cs of CASES) { if (!present.some(c => m[c.key(cs)])) continue; h += `` + present.map(c => cell(m[c.key(cs)], cur)).join('') + ''; } return h + '
${esc(c.label)}
${t.case[cs]}
'; } function nounTables(e, cur, lang) { const m = index(e.forms); const t = L[lang]; const cols = [ { label: t.sg, key: c => c + 'ET' }, { label: t.sgdef, key: c => c + 'ETgr' }, { label: t.pl, key: c => c + 'FT' }, { label: t.pldef, key: c => c + 'FTgr' } ]; return caseTable(m, cur, lang, cols, ''); } // Declension with gender columns, grouped by a leading code (adjectives, // gendered pronouns/numerals, past participles). function declTables(forms, cur, lang) { const t = L[lang]; // group -> gender -> number -> caseCode -> [forms] const groups = {}; const order = []; for (const [tag, f] of forms) { const parts = norm(tag).split('-'); const gender = parts.find(p => p in t.gender) || '_'; let cn = null; for (const p of parts) { const mm = p.match(/^(NF|ÞF|ÞGF|EF)(ET|FT)$/); if (mm) { cn = mm; break; } } if (!cn) continue; const cas = cn[1], num = cn[2]; const rest = parts.filter(p => p !== gender && p !== cn[0] && !['GM', 'MM', 'LH', 'LHÞT', 'SP', 'OP'].includes(p)); const g = rest.join('-'); if (!(g in groups)) { groups[g] = {}; order.push(g); } const key = gender + ':' + cas + ':' + num; (groups[g][key] = groups[g][key] || []).push(f); } const prio = ['FSB', 'FVB', 'FST', 'MST', 'EST', 'ESB', 'EVB', 'SB', 'VB', '']; order.sort((a, b) => { const ia = prio.indexOf(a), ib = prio.indexOf(b); return (ia < 0 ? 99 : ia) - (ib < 0 ? 99 : ib); }); let html = ''; for (const g of order) { const map = groups[g]; const genders = ['KK', 'KVK', 'HK'].filter(gd => CASES.some(c => map[gd + ':' + c + ':ET'] || map[gd + ':' + c + ':FT'])); const useGender = genders.length > 0; const heading = t.deg[g] || g; for (const num of ['ET', 'FT']) { const numLabel = num === 'ET' ? t.sg : t.pl; if (useGender) { const cols = genders.map(gd => ({ gd })); const has = CASES.some(c => genders.some(gd => map[gd + ':' + c + ':' + num])); if (!has) continue; html += `
${esc([heading, numLabel].filter(Boolean).join(' · '))}
`; html += '' + genders.map(gd => ``).join('') + ''; for (const c of CASES) { if (!genders.some(gd => map[gd + ':' + c + ':' + num])) continue; html += `` + genders.map(gd => cell(map[gd + ':' + c + ':' + num], cur)).join('') + ''; } html += '
${t.gender[gd]}
${t.case[c]}
'; } else { const has = CASES.some(c => map['_:' + c + ':' + num]); if (!has) continue; html += `
${esc([heading, numLabel].filter(Boolean).join(' · '))}
`; html += ''; for (const c of CASES) { if (!map['_:' + c + ':' + num]) continue; html += `` + cell(map['_:' + c + ':' + num], cur) + ''; } html += '
${t.case[c]}
'; } } } return html; } function line(label, forms, cur) { if (!forms || !forms.length) return ''; const html = forms.map(f => f.toLowerCase() === cur ? `${esc(f)}` : esc(f)).join(' / '); return `
${esc(label)}${html}
`; } function verbTables(e, cur, lang) { const t = L[lang]; const m = index(e.forms); const get = k => m[k]; let html = ''; // Non-finite html += line(t.infinitive, get('GM-NH') || get('NH'), cur); html += line(t.infinitive + ' (MM)', get('MM-NH'), cur); html += line(t.prespart, get('LHNT'), cur); html += line(t.supine, get('GM-SAGNB') || get('SAGNB') || get('MM-SAGNB'), cur); // Imperative (any BH tag) const imp = []; for (const [tag, f] of e.forms) if (/BH/.test(tag)) imp.push(f); html += line(t.imperative, imp, cur); // Finite tables: voice · mood · tense, rows persons × cols sg/pl for (const voice of ['GM', 'MM']) { for (const mood of ['FH', 'VH']) { for (const tense of ['NT', 'ÞT']) { const rows = ['1P', '2P', '3P'].map(p => ({ p, et: get([voice, mood, tense, p, 'ET'].join('-')), ft: get([voice, mood, tense, p, 'FT'].join('-')) })); if (!rows.some(r => r.et || r.ft)) continue; const hasPl = rows.some(r => r.ft); html += `
${[t.voice[voice], t.mood[mood], t.tense[tense]].join(' · ')}
`; html += '' + (hasPl ? '' : '') + ''; for (const r of rows) { if (!r.et && !r.ft) continue; html += `` + cell(r.et, cur) + (hasPl ? cell(r.ft, cur) : '') + ''; } html += '
' + t.sg + '' + t.pl + '
${t.person[r.p]}
'; } } } // Past participle (declines like an adjective) const pp = e.forms.filter(([tag]) => /LHÞT/.test(tag)); if (pp.length) { html += `
${t.pastpart}
`; html += declTables(pp.map(([tag, f]) => [tag.replace(/.*LHÞT-?/, ''), f]), cur, lang); } return html; } function fallbackList(e, cur, lang) { return '' + e.forms.map(([tag, f]) => `` + cell([f], cur) + '').join('') + '
${esc(tag)}
'; } function renderParadigm(e, currentForm, lang) { const cur = (currentForm || '').toLowerCase(); if (!e || !e.forms || !e.forms.length) return ''; if (e.forms.length === 1 && /OBEYGJANLEGT/.test(e.forms[0][0])) { return `
${esc(e.forms[0][1])}${L[lang].indeclinable}
`; } if (NOUN[e.ofl]) return nounTables(e, cur, lang); if (e.ofl === 'so') return verbTables(e, cur, lang); if (e.ofl === 'lo') return declTables(e.forms, cur, lang); // gendered pronoun / numeral? if (e.forms.some(([tag]) => /(^|-)(KK|KVK|HK)(-|$)/.test(tag))) return declTables(e.forms, cur, lang); // case-inflected without gender (personal pronouns etc.) if (e.forms.some(([tag]) => /^(NF|ÞF|ÞGF|EF)(ET|FT)/.test(norm(tag)))) return nounTables(e, cur, lang); return fallbackList(e, cur, lang); } global.renderParadigm = renderParadigm; })(typeof window !== 'undefined' ? window : globalThis);