This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| font:wordconstuct [2026/01/17 14:36] – removed - external edit (Unknown date) A User Not Logged in | font:wordconstuct [2026/03/24 22:28] (current) – external edit A User Not Logged in | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| + | ====== Yivalese Word Construction ====== | ||
| + | < | ||
| + | < | ||
| + | /* Dark Theme Optimized Styling */ | ||
| + | .yiv-container { | ||
| + | color: inherit; | ||
| + | padding: 10px; | ||
| + | font-family: | ||
| + | } | ||
| + | |||
| + | /* Matrix grid hidden - using link-list only */ | ||
| + | .matrix-grid { | ||
| + | display: none; | ||
| + | } | ||
| + | |||
| + | .initial-cell { | ||
| + | display: none; | ||
| + | } | ||
| + | |||
| + | .pair-btn { | ||
| + | display: none; | ||
| + | } | ||
| + | |||
| + | .output-section { | ||
| + | margin-top: 40px; | ||
| + | padding: 20px; | ||
| + | border: 1px solid rgba(255, | ||
| + | background: rgba(0, | ||
| + | border-radius: | ||
| + | } | ||
| + | # | ||
| + | width: 100%; | ||
| + | background: rgba(255, | ||
| + | color: #fff; | ||
| + | border: 1px solid rgba(255, | ||
| + | padding: 12px; | ||
| + | font-size: 1.3em; | ||
| + | margin-bottom: | ||
| + | border-radius: | ||
| + | } | ||
| + | .preview-label { | ||
| + | font-size: 0.75em; | ||
| + | text-transform: | ||
| + | letter-spacing: | ||
| + | color: rgba(255, | ||
| + | margin-bottom: | ||
| + | } | ||
| + | # | ||
| + | font-size: 6em; | ||
| + | line-height: | ||
| + | margin-bottom: | ||
| + | min-height: 0.7em; | ||
| + | } | ||
| + | # | ||
| + | font-size: 1.2em; | ||
| + | color: #9cdcfe; | ||
| + | font-style: italic; | ||
| + | min-height: 1.2em; | ||
| + | } | ||
| + | |||
| + | .yiv-font { font-family: | ||
| + | |||
| + | .reading-list { | ||
| + | min-height: 2.8em; | ||
| + | background: rgba(255, | ||
| + | border: 1px solid rgba(255, | ||
| + | border-radius: | ||
| + | padding: 10px 12px; | ||
| + | font-size: 1.1em; | ||
| + | line-height: | ||
| + | color: #fff; | ||
| + | margin-bottom: | ||
| + | display: flex; | ||
| + | flex-direction: | ||
| + | } | ||
| + | |||
| + | .reading-items { | ||
| + | display: flex; | ||
| + | flex-direction: | ||
| + | gap: 6px; | ||
| + | } | ||
| + | |||
| + | /* show appended readings on the same line */ | ||
| + | .reading-chunk { display: inline; } | ||
| + | |||
| + | .pair-description { | ||
| + | font-size: 0.95em; | ||
| + | opacity: 0.9; | ||
| + | font-weight: | ||
| + | } | ||
| + | |||
| + | /* workflow visuals */ | ||
| + | .workflow-grid { | ||
| + | display: grid; | ||
| + | grid-template-columns: | ||
| + | gap: 8px; | ||
| + | margin-bottom: | ||
| + | } | ||
| + | .workflow-card { | ||
| + | border: 1px solid rgba(255, | ||
| + | border-radius: | ||
| + | padding: 12px; | ||
| + | background: rgba(255, | ||
| + | display: flex; | ||
| + | flex-direction: | ||
| + | gap: 4px; | ||
| + | } | ||
| + | .workflow-card.center { | ||
| + | background: rgba(156, 220, 254, 0.08); | ||
| + | border-color: | ||
| + | } | ||
| + | .workflow-label { | ||
| + | font-size: 0.8em; | ||
| + | text-transform: | ||
| + | letter-spacing: | ||
| + | color: #9cdcfe; | ||
| + | } | ||
| + | .workflow-example { | ||
| + | font-size: 1em; | ||
| + | font-weight: | ||
| + | } | ||
| + | |||
| + | /* Responsive tweak: on small screens make workflow cards narrower and scrollable */ | ||
| + | @media (max-width: 600px) { | ||
| + | .workflow-grid { | ||
| + | /* keep grid layout but tighten minimum widths so cards always fit */ | ||
| + | grid-template-columns: | ||
| + | gap: 6px; | ||
| + | padding-bottom: | ||
| + | } | ||
| + | .workflow-card { | ||
| + | min-width: 0; /* allow flexible shrinking */ | ||
| + | padding: 6px; | ||
| + | font-size: 0.95em; | ||
| + | } | ||
| + | .workflow-label { | ||
| + | font-size: 0.72em; | ||
| + | } | ||
| + | .workflow-example { | ||
| + | font-size: 0.88em; | ||
| + | } | ||
| + | } | ||
| + | |||
| + | /* Link list always visible */ | ||
| + | .mobile-menu { | ||
| + | margin-bottom: | ||
| + | display: flex !important; | ||
| + | flex-direction: | ||
| + | gap: 12px; | ||
| + | } | ||
| + | .mobile-category { | ||
| + | margin: 0; | ||
| + | } | ||
| + | .mobile-category h4 { | ||
| + | margin: 0 0 6px 0; | ||
| + | font-size: 0.95em; | ||
| + | color: #9cdcfe; | ||
| + | } | ||
| + | .mobile-link { | ||
| + | margin-right: | ||
| + | color: #9cdcfe; | ||
| + | text-decoration: | ||
| + | cursor: pointer; | ||
| + | display: inline-block; | ||
| + | } | ||
| + | |||
| + | /* Logogram preview */ | ||
| + | .logogram-preview { | ||
| + | position: relative; | ||
| + | width: 100px; | ||
| + | height: 100px; | ||
| + | border: 1px solid rgba(255, | ||
| + | border-radius: | ||
| + | background: rgba(0, | ||
| + | display: flex; | ||
| + | align-items: | ||
| + | justify-content: | ||
| + | font-family: | ||
| + | margin-bottom: | ||
| + | padding: 2px; | ||
| + | } | ||
| + | .logogram-main { | ||
| + | font-size: 8.5em; | ||
| + | line-height: | ||
| + | display: | ||
| + | text-align: | ||
| + | transform: translateY(-8%); | ||
| + | } | ||
| + | .logogram-truncate-note { | ||
| + | font-size: 0.85em; | ||
| + | color: rgba(255, | ||
| + | margin-top: 6px; | ||
| + | } | ||
| + | |||
| + | .reading-hint { | ||
| + | color: rgba(255, | ||
| + | font-size: 0.85em; | ||
| + | letter-spacing: | ||
| + | } | ||
| + | |||
| + | .more-link { | ||
| + | display: none; | ||
| + | margin-top: 6px; | ||
| + | align-self: flex-end; | ||
| + | color: #9cdcfe; | ||
| + | font-size: 0.85em; | ||
| + | text-decoration: | ||
| + | cursor: pointer; | ||
| + | } | ||
| + | .more-link: | ||
| + | text-decoration: | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | <div class=" | ||
| + | <div id=" | ||
| + | <div class=" | ||
| + | <div class=" | ||
| + | <div class=" | ||
| + | </ | ||
| + | <div class=" | ||
| + | <div class=" | ||
| + | <div class=" | ||
| + | </ | ||
| + | <div class=" | ||
| + | <div class=" | ||
| + | <div class=" | ||
| + | </ | ||
| + | </ | ||
| + | <div class=" | ||
| + | <div class=" | ||
| + | <div id=" | ||
| + | <div id=" | ||
| + | |||
| + | <div class=" | ||
| + | <div class=" | ||
| + | <div style=" | ||
| + | <input type=" | ||
| + | <button id=" | ||
| + | <button id=" | ||
| + | </ | ||
| + | | ||
| + | <div style=" | ||
| + | <div> | ||
| + | <div class=" | ||
| + | <div id=" | ||
| + | </ | ||
| + | <div> | ||
| + | <div class=" | ||
| + | <div id=" | ||
| + | <div class=" | ||
| + | <span class=" | ||
| + | </ | ||
| + | <div class=" | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| + | | ||
| + | <div class=" | ||
| + | <div id=" | ||
| + | | ||
| + | <div class=" | ||
| + | <div id=" | ||
| + | <div id=" | ||
| + | <span class=" | ||
| + | </ | ||
| + | <a href="#" | ||
| + | </ | ||
| + | <div class=" | ||
| + | <div id=" | ||
| + | <div id=" | ||
| + | <span class=" | ||
| + | </ | ||
| + | <div class=" | ||
| + | </ | ||
| + | | ||
| + | | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | < | ||
| + | const data = { | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | }; | ||
| + | |||
| + | const phonoUrl = '/ | ||
| + | let phoneticMap = {}; | ||
| + | let glossSet = null; | ||
| + | let glossMap = null; | ||
| + | let candidateSets = []; | ||
| + | const chunkSize = 20; | ||
| + | const maxComboSamples = 500; | ||
| + | let shuffledCombos = []; | ||
| + | let comboCursor = 0; | ||
| + | let totalCombos = 0; | ||
| + | |||
| + | const root = document.getElementById(' | ||
| + | const input = document.getElementById(' | ||
| + | const gPreview = document.getElementById(' | ||
| + | const mPreview = document.getElementById(' | ||
| + | const readingItems = document.getElementById(' | ||
| + | const moreLink = document.getElementById(' | ||
| + | const shareBtn = document.getElementById(' | ||
| + | const clearInline = document.getElementById(' | ||
| + | |||
| + | /* Mobile/menu behavior: render a compact categorized list instead of grid on small screens */ | ||
| + | function renderMobileMenu() { | ||
| + | const menuContainer = document.getElementById(' | ||
| + | if (!menuContainer) return; | ||
| + | menuContainer.innerHTML = ''; | ||
| + | |||
| + | const menu = document.createElement(' | ||
| + | menu.id = ' | ||
| + | menu.style.display = ' | ||
| + | menu.style.flexDirection = ' | ||
| + | // Build exhaustive categories from `data` using keyword matching | ||
| + | function buildCategories() { | ||
| + | const keywordMap = [ | ||
| + | { name: ' | ||
| + | { name: ' | ||
| + | { name: ' | ||
| + | { name: ' | ||
| + | { name: ' | ||
| + | ]; | ||
| + | |||
| + | const cats = {}; | ||
| + | for (const init in data) { | ||
| + | const col = data[init]; | ||
| + | for (const fin in col) { | ||
| + | const meaning = col[fin]; | ||
| + | const pair = init + fin; | ||
| + | const m = ('' | ||
| + | let assigned = false; | ||
| + | for (const map of keywordMap) { | ||
| + | for (const kw of map.keywords) { | ||
| + | if (m.includes(kw)) { | ||
| + | cats[map.name] = cats[map.name] || []; | ||
| + | cats[map.name].push({ label: meaning, pair }); | ||
| + | assigned = true; | ||
| + | break; | ||
| + | } | ||
| + | } | ||
| + | if (assigned) break; | ||
| + | } | ||
| + | if (!assigned) { | ||
| + | cats[' | ||
| + | cats[' | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | return cats; | ||
| + | } | ||
| + | |||
| + | const categories = buildCategories(); | ||
| + | |||
| + | Object.keys(categories).forEach(cat => { | ||
| + | const wrap = document.createElement(' | ||
| + | wrap.className = ' | ||
| + | const h = document.createElement(' | ||
| + | h.textContent = cat; | ||
| + | wrap.appendChild(h); | ||
| + | categories[cat].forEach(item => { | ||
| + | const a = document.createElement(' | ||
| + | a.className = ' | ||
| + | a.href = '#'; | ||
| + | a.textContent = item.label + ' (' + item.pair + ' | ||
| + | a.addEventListener(' | ||
| + | e.preventDefault(); | ||
| + | input.value += item.pair; | ||
| + | sync(); | ||
| + | }); | ||
| + | wrap.appendChild(a); | ||
| + | }); | ||
| + | menu.appendChild(wrap); | ||
| + | }); | ||
| + | |||
| + | // insert menu into the container (replace old content) | ||
| + | menuContainer.appendChild(menu); | ||
| + | } | ||
| + | |||
| + | window.addEventListener(' | ||
| + | window.addEventListener(' | ||
| + | |||
| + | // Prefill `latin-output` from ?glyph=... if present (URLSearchParams returns decoded value) | ||
| + | function applyQueryPrefill() { | ||
| + | try { | ||
| + | const params = new URLSearchParams(window.location.search); | ||
| + | const glyph = params.get(' | ||
| + | if (glyph) { | ||
| + | input.value = glyph; | ||
| + | } | ||
| + | } catch (e) { | ||
| + | // ignore malformed | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // Share button: copy a URL with encoded glyph param to clipboard | ||
| + | if (shareBtn) { | ||
| + | shareBtn.addEventListener(' | ||
| + | e.preventDefault(); | ||
| + | const base = window.location.origin + window.location.pathname; | ||
| + | const encoded = encodeURIComponent(input.value || '' | ||
| + | const url = base + (encoded ? ('? | ||
| + | if (navigator.clipboard && navigator.clipboard.writeText) { | ||
| + | navigator.clipboard.writeText(url).then(() => { | ||
| + | const prev = shareBtn.textContent; | ||
| + | shareBtn.textContent = ' | ||
| + | setTimeout(() => { shareBtn.textContent = prev; }, 1500); | ||
| + | }).catch(() => { | ||
| + | // fallback: prompt | ||
| + | window.prompt(' | ||
| + | }); | ||
| + | } else { | ||
| + | window.prompt(' | ||
| + | } | ||
| + | }); | ||
| + | } | ||
| + | |||
| + | if (clearInline) { | ||
| + | clearInline.addEventListener(' | ||
| + | e.preventDefault(); | ||
| + | clearAll(); | ||
| + | }); | ||
| + | } | ||
| + | |||
| + | fetch(phonoUrl) | ||
| + | .then(resp => resp.ok ? resp.json() : Promise.reject()) | ||
| + | .then(json => { | ||
| + | phoneticMap = json; | ||
| + | updateReadings(input.value); | ||
| + | }) | ||
| + | .catch(() => { | ||
| + | readingItems.innerHTML = '< | ||
| + | moreLink.style.display = ' | ||
| + | }); | ||
| + | |||
| + | // Load glossary lookup JSON (dumped from server) for existence checks | ||
| + | fetch('/ | ||
| + | .then(r => r.ok ? r.json() : Promise.reject()) | ||
| + | .then(list => { | ||
| + | try { | ||
| + | glossMap = new Map(); | ||
| + | (list || []).forEach(o => { | ||
| + | if (!o || !o.s) return; | ||
| + | const key = ('' | ||
| + | const meaning = (o.m || '' | ||
| + | glossMap.set(key, | ||
| + | }); | ||
| + | glossSet = new Set(glossMap.keys()); | ||
| + | } catch (e) { | ||
| + | glossSet = null; | ||
| + | glossMap = null; | ||
| + | } | ||
| + | // if phoneticMap already loaded, refresh readings to mark exists | ||
| + | updateReadings(input.value); | ||
| + | }) | ||
| + | .catch(() => { glossSet = null; glossMap = null; }); | ||
| + | |||
| + | // `more` link behavior is set when rendering previews below. | ||
| + | |||
| + | Object.keys(data).forEach(init => { | ||
| + | const head = document.createElement(' | ||
| + | head.className = ' | ||
| + | head.innerHTML = ` | ||
| + | <span class=" | ||
| + | <span style=" | ||
| + | `; | ||
| + | root.appendChild(head); | ||
| + | |||
| + | const pairs = data[init]; | ||
| + | Object.keys(pairs).forEach(fin => { | ||
| + | const pair = init + fin; | ||
| + | const btn = document.createElement(' | ||
| + | btn.className = ' | ||
| + | btn.onclick = () => { input.value += pair; sync(); }; | ||
| + | btn.innerHTML = ` | ||
| + | <span class=" | ||
| + | < | ||
| + | <span class=" | ||
| + | `; | ||
| + | root.appendChild(btn); | ||
| + | }); | ||
| + | }); | ||
| + | |||
| + | renderMobileMenu(); | ||
| + | |||
| + | function sync() { | ||
| + | const val = input.value; | ||
| + | // Single character view: preserve entered case (font handles display) | ||
| + | gPreview.innerText = val; | ||
| + | | ||
| + | let meanings = []; | ||
| + | for (let i = 0; i < val.length; i += 2) { | ||
| + | const pair = val.substr(i, | ||
| + | if (pair.length === 2) { | ||
| + | const init = pair[0].toUpperCase(); | ||
| + | const fin = pair[1].toLowerCase(); | ||
| + | if (data[init] && data[init][fin]) { | ||
| + | meanings.push(data[init][fin]); | ||
| + | } else { | ||
| + | meanings.push("?" | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | mPreview.innerText = meanings.join(' | ||
| + | | ||
| + | // Generate logogram | ||
| + | generateLogogram(val); | ||
| + | | ||
| + | updateReadings(val); | ||
| + | } | ||
| + | |||
| + | function generateLogogram(val) { | ||
| + | const logogramMain = document.getElementById(' | ||
| + | const logogramNote = document.getElementById(' | ||
| + | |||
| + | if (!val || val.length === 0) { | ||
| + | logogramMain.textContent = ''; | ||
| + | if (logogramNote) logogramNote.style.display = ' | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | const glyphs = []; | ||
| + | for (let i = 0; i < val.length; i += 2) { | ||
| + | const pair = val.substr(i, | ||
| + | if (pair.length === 2) glyphs.push(pair); | ||
| + | } | ||
| + | |||
| + | if (!glyphs.length) { | ||
| + | logogramMain.textContent = ''; | ||
| + | if (logogramNote) logogramNote.style.display = ' | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | const truncated = glyphs.length > 3; | ||
| + | const used = glyphs.slice(0, | ||
| + | const display = used[0].toUpperCase() + used.slice(1).map(p => p.toLowerCase()).join('' | ||
| + | logogramMain.textContent = display; | ||
| + | if (logogramNote) logogramNote.style.display = truncated ? ' | ||
| + | } | ||
| + | |||
| + | function applyPhoneticVariants(reading) { | ||
| + | const variants = [reading]; | ||
| + | let modified = reading; | ||
| + | | ||
| + | // g ↔ k mapping | ||
| + | if (modified.includes(' | ||
| + | const toggled = modified.replace(/ | ||
| + | if (toggled !== modified) variants.push(toggled); | ||
| + | } | ||
| + | | ||
| + | // n ↔ m mapping removed: avoid producing m-substituted variants | ||
| + | | ||
| + | // f ↔ p mapping | ||
| + | if (modified.includes(' | ||
| + | const toggled = modified.replace(/ | ||
| + | if (toggled !== modified) variants.push(toggled); | ||
| + | } | ||
| + | | ||
| + | // f ↔ b mapping (alternate) | ||
| + | if (modified.includes(' | ||
| + | const toggled = modified.replace(/ | ||
| + | if (toggled !== modified) variants.push(toggled); | ||
| + | } | ||
| + | | ||
| + | return variants; | ||
| + | } | ||
| + | |||
| + | // Generate fuzzy candidate normalizations for lookup checks | ||
| + | function generateFuzzyCandidates(word) { | ||
| + | if (!word) return []; | ||
| + | const w = ('' | ||
| + | const variants = new Set(); | ||
| + | variants.add(w); | ||
| + | |||
| + | // collapse doubled letters (pedakk -> pedak) | ||
| + | variants.add(w.replace(/ | ||
| + | |||
| + | // remove all h's (peddakh -> peddak -> peddak -> pedak) | ||
| + | variants.add(w.replace(/ | ||
| + | variants.add(w.replace(/ | ||
| + | |||
| + | // map common aspirated pairs to plain (dh->d, kh->k, ph->p, th->t, gh->g, bh->b) | ||
| + | variants.add(w.replace(/ | ||
| + | variants.add(w.replace(/ | ||
| + | variants.add(w.replace(/ | ||
| + | variants.add(w.replace(/ | ||
| + | variants.add(w.replace(/ | ||
| + | variants.add(w.replace(/ | ||
| + | |||
| + | // remove trailing single h | ||
| + | variants.add(w.replace(/ | ||
| + | |||
| + | // also consider single <-> double n variants (nn <-> n) | ||
| + | variants.add(w.replace(/ | ||
| + | variants.add(w.replace(/ | ||
| + | |||
| + | return Array.from(variants); | ||
| + | } | ||
| + | |||
| + | function updateReadings(value) { | ||
| + | candidateSets = []; | ||
| + | comboCursor = 0; | ||
| + | readingItems.innerHTML = ''; | ||
| + | const pairs = []; | ||
| + | |||
| + | for (let i = 0; i < value.length; | ||
| + | const group = value.substr(i, | ||
| + | if (group.length !== 2) { | ||
| + | continue; | ||
| + | } | ||
| + | pairs.push(group); | ||
| + | } | ||
| + | |||
| + | if (!pairs.length) { | ||
| + | readingItems.innerHTML = '< | ||
| + | moreLink.style.display = ' | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | candidateSets = pairs.map(getCandidates); | ||
| + | const rawCombos = buildCombos(candidateSets); | ||
| + | const uniqueCombos = Array.from(new Set(rawCombos.map(capitalizeReading))).filter(x => x && !x.includes(' | ||
| + | | ||
| + | // Add phonetic variants | ||
| + | let expandedCombos = []; | ||
| + | for (const combo of uniqueCombos) { | ||
| + | expandedCombos.push(combo); | ||
| + | const variants = applyPhoneticVariants(combo); | ||
| + | for (const variant of variants) { | ||
| + | if (variant !== combo && !expandedCombos.includes(variant)) { | ||
| + | expandedCombos.push(variant); | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | | ||
| + | shuffledCombos = shuffleArray(expandedCombos); | ||
| + | if (shuffledCombos.length > maxComboSamples) { | ||
| + | shuffledCombos = shuffledCombos.slice(0, | ||
| + | } | ||
| + | totalCombos = shuffledCombos.length; | ||
| + | if (!totalCombos) { | ||
| + | readingItems.innerHTML = '< | ||
| + | moreLink.style.display = ' | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | // Populate " | ||
| + | const existingItems = document.getElementById(' | ||
| + | const existingNote = document.getElementById(' | ||
| + | if (existingItems) existingItems.innerHTML = ''; | ||
| + | // ensure matchedCombos exists even if glossSet is unavailable | ||
| + | let matchedCombos = new Set(); | ||
| + | if (glossSet && existingItems) { | ||
| + | // collect the actual DB keys that matched any fuzzy variant | ||
| + | const foundDB = new Set(); | ||
| + | matchedCombos = new Set(); | ||
| + | for (const combo of shuffledCombos) { | ||
| + | const checks = generateFuzzyCandidates(combo); | ||
| + | for (const c of checks) { | ||
| + | if (glossSet.has(c)) { foundDB.add(c); | ||
| + | } | ||
| + | } | ||
| + | if (foundDB.size) { | ||
| + | // render as a single inline, comma-delimited list with links and meanings | ||
| + | existingItems.innerHTML = ''; | ||
| + | const span = document.createElement(' | ||
| + | span.className = ' | ||
| + | let first = true; | ||
| + | Array.from(foundDB).forEach(dbKey => { | ||
| + | if (!first) span.appendChild(document.createTextNode(', | ||
| + | first = false; | ||
| + | const a = document.createElement(' | ||
| + | a.href = '/ | ||
| + | const meaning = (glossMap && glossMap.get(dbKey)) ? glossMap.get(dbKey) : ''; | ||
| + | a.textContent = dbKey.charAt(0).toUpperCase() + dbKey.slice(1) + (meaning ? (' (' + meaning + ' | ||
| + | a.style.color = '# | ||
| + | span.appendChild(a); | ||
| + | }); | ||
| + | existingItems.appendChild(span); | ||
| + | if (existingNote) existingNote.style.display = ' | ||
| + | } else { | ||
| + | existingItems.innerHTML = '< | ||
| + | if (existingNote) existingNote.style.display = ' | ||
| + | } | ||
| + | } else if (existingItems) { | ||
| + | existingItems.innerHTML = '< | ||
| + | if (existingNote) existingNote.style.display = ' | ||
| + | } | ||
| + | |||
| + | // Remove any example readings that matched the DB so they don't appear twice | ||
| + | if (matchedCombos.size) { | ||
| + | shuffledCombos = shuffledCombos.filter(c => !matchedCombos.has(c)); | ||
| + | totalCombos = shuffledCombos.length; | ||
| + | if (!totalCombos) { | ||
| + | readingItems.innerHTML = '< | ||
| + | moreLink.style.display = ' | ||
| + | return; | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // Render a simple preview: show up to `previewLimit` items, then reveal more on demand. | ||
| + | const separator = ', '; | ||
| + | const previewLimit = 50; | ||
| + | |||
| + | let line = readingItems.querySelector(' | ||
| + | if (!line) { | ||
| + | line = document.createElement(' | ||
| + | line.className = ' | ||
| + | readingItems.appendChild(line); | ||
| + | } | ||
| + | |||
| + | const previewCount = Math.min(previewLimit, | ||
| + | line.textContent = shuffledCombos.slice(0, | ||
| + | |||
| + | if (shuffledCombos.length > previewCount) { | ||
| + | moreLink.style.display = ' | ||
| + | moreLink.textContent = ' | ||
| + | moreLink.onclick = (e) => { e.preventDefault(); | ||
| + | } else { | ||
| + | moreLink.style.display = ' | ||
| + | } | ||
| + | } | ||
| + | |||
| + | function getCandidates(pair) { | ||
| + | if (!pair) { | ||
| + | return ['' | ||
| + | } | ||
| + | const normalized = pair.toLowerCase(); | ||
| + | const list = Array.isArray(phoneticMap[normalized]) ? phoneticMap[normalized].slice() : []; | ||
| + | const filtered = list.filter(s => typeof s === ' | ||
| + | if (!filtered.length) { | ||
| + | return [pair[0].toUpperCase() + pair[1].toLowerCase()]; | ||
| + | } | ||
| + | const baseLetter = pair[0].toUpperCase(); | ||
| + | const preferred = filtered.filter(s => s[0].toUpperCase() === baseLetter); | ||
| + | if (preferred.length) { | ||
| + | const others = filtered.filter(s => s[0].toUpperCase() !== baseLetter); | ||
| + | return preferred.concat(others); | ||
| + | } | ||
| + | return filtered; | ||
| + | } | ||
| + | |||
| + | function buildCombos(sets) { | ||
| + | if (!sets.length) { | ||
| + | return []; | ||
| + | } | ||
| + | return sets.reduce((acc, | ||
| + | if (!acc.length) { | ||
| + | return set.slice(); | ||
| + | } | ||
| + | const next = []; | ||
| + | acc.forEach(prefix => { | ||
| + | set.forEach(token => { | ||
| + | next.push(prefix + token); | ||
| + | }); | ||
| + | }); | ||
| + | return next; | ||
| + | }, []); | ||
| + | } | ||
| + | |||
| + | function loadNextChunk() { | ||
| + | if (!shuffledCombos.length || comboCursor >= totalCombos) { | ||
| + | moreLink.style.display = ' | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | const end = Math.min(comboCursor + chunkSize, totalCombos); | ||
| + | const segment = shuffledCombos.slice(comboCursor, | ||
| + | const text = segment.join(', | ||
| + | let line = readingItems.querySelector(' | ||
| + | if (!line) { | ||
| + | line = document.createElement(' | ||
| + | line.className = ' | ||
| + | line.textContent = text; | ||
| + | readingItems.appendChild(line); | ||
| + | } else { | ||
| + | // append with comma separator (avoid leading comma) | ||
| + | if (line.textContent && line.textContent.trim().length) { | ||
| + | line.textContent = line.textContent + ', ' + text; | ||
| + | } else { | ||
| + | line.textContent = text; | ||
| + | } | ||
| + | } | ||
| + | |||
| + | comboCursor = end; | ||
| + | moreLink.style.display = comboCursor < totalCombos ? ' | ||
| + | } | ||
| + | |||
| + | function shuffleArray(items) { | ||
| + | const array = items.slice(); | ||
| + | for (let i = array.length - 1; i > 0; i--) { | ||
| + | const j = Math.floor(Math.random() * (i + 1)); | ||
| + | [array[i], array[j]] = [array[j], array[i]]; | ||
| + | } | ||
| + | return array; | ||
| + | } | ||
| + | |||
| + | function capitalizeReading(text) { | ||
| + | if (!text) { | ||
| + | return ''; | ||
| + | } | ||
| + | return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase(); | ||
| + | } | ||
| + | |||
| + | function clearAll() { | ||
| + | input.value = ''; | ||
| + | sync(); | ||
| + | } | ||
| + | |||
| + | // apply any glyph param before first sync | ||
| + | applyQueryPrefill(); | ||
| + | readingItems.innerHTML = '< | ||
| + | sync(); | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | ===== Linguistic Construction: | ||
| + | |||
| + | Writing Yivalese from the spoken form into its written one is an interpretive process. This is due to phonetic and semantic drift from its original source which was fairly consistent, until it wasn' | ||
| + | |||
| + | Once you have selected a compound of glyphs to represent a concept - such as GxDl (< | ||