diff --git a/app.js b/app.js index 149bd3d..6ca681b 100644 --- a/app.js +++ b/app.js @@ -29,12 +29,12 @@ function playSpeaker(freq = 800, duration = 0.04, type = 'square') { } catch (e) {} } -// Data references +// Data references loaded from data.js const KNIHA_RECEPTU = window.KNIHA_RECEPTU || []; const HISTORICKA_JIDLA = window.HISTORICKA_JIDLA || []; // App State -let currentView = 'menu'; // 'menu', 'recept', 'recepty_list', 'suroviny', 'davno', 'oblibena', 'kronika', 'altan' +let currentView = 'menu'; // 'menu', 'recept', 'recepty_list', 'hledat', 'suroviny', 'davno', 'oblibena', 'kronika', 'altan' let currentRecipeId = 1; let currentCategory = 'Vše'; let listPage = 0; @@ -50,11 +50,163 @@ setInterval(() => { timeEl.textContent = d.toTimeString().split(' ')[0]; }, 1000); -// Normalization function (strip accents & non-alphanumeric) +// ========================================================================= +// CZECH TEXT NORMALIZATION & FULLTEXT SEARCH ENGINE +// ========================================================================= + +// Normalizace: malá písmena, odstranění diakritiky function norm(str) { return (str || "").toLowerCase() - .normalize("NFD").replace(/[\u0300-\u036f]/g, "") - .replace(/[^a-z0-9]/g, ""); + .normalize("NFD").replace(/[\u0300-\u036f]/g, ""); +} + +// Český lehký stemmer (odstranění pádových a slovesných koncovek pro dokonalou flexi) +function stem(word) { + let w = norm(word).replace(/[^a-z0-9]/g, ""); + if (w.length <= 3) return w; + // Dlouhé přídavné a slovesné koncovky + w = w.replace(/(ovy|ova|ove|ymi|ych|ich|ovi|ami|ach|ech|eho|emu|ajici|ejici)$/, ""); + if (w.length > 3) { + // Slovesa a podstatná jména + w = w.replace(/(ime|ite|eme|ete|aji|eji|ala|alo|ali|ila|ilo|ili|ula|ulo|uli)$/, ""); + } + if (w.length > 3) { + // Dvojpísmenné koncovky + w = w.replace(/(em|ou|am|um|ym|im|ky|ka|ku|ek|ce|ci|ho|mu|mi|es|as|at|et|it|ut|ti|al|il|ul|ne|na|ny)$/, ""); + } + if (w.length > 3) { + // Krátké samohlásky + w = w.replace(/[aeiouy]$/, ""); + } + return w; +} + +// Rozdělení dotazu na čistá slova +function tokenize(str) { + return norm(str) + .split(/[\s,.;:()!?"'\/\\+*#%&<=>[\]{}|~_`^$]+/) + .filter(t => t.length > 0); +} + +// Kontrola shody slova: +function wordMatches(targetWord, queryWord, queryStem) { + const tw = norm(targetWord); + if (tw === queryWord) return true; + if (tw.startsWith(queryWord) || queryWord.startsWith(tw)) return true; + + const ts = stem(targetWord); + if (ts === queryStem) return true; + if (queryStem.length >= 3 && ts.startsWith(queryStem)) return true; + if (ts.length >= 3 && queryStem.startsWith(ts)) return true; + return false; +} + +// Zvýraznění shod v textu (HTML mark) +function highlightMatches(text, query) { + if (!query || !text) return text; + const tokens = tokenize(query).filter(t => t.length > 1); + if (tokens.length === 0) return text; + + const stems = tokens.map(stem); + const parts = text.split(/(\s+|[.,;()\-+*\/:"'!?<>])/); + + return parts.map(part => { + if (!part.trim()) return part; + const np = norm(part); + const sp = stem(part); + const isMatch = tokens.some((tok, idx) => wordMatches(part, tok, stems[idx])); + if (isMatch) { + return `${part}`; + } + return part; + }).join(""); +} + +// Robustní vyhledávání v sešitu Kamily Pešek +function searchKniha(query, categoryFilter = 'Vše') { + if (!query || !query.trim()) return []; + const tokens = tokenize(query); + if (tokens.length === 0) return []; + const stems = tokens.map(stem); + + const source = categoryFilter === 'Vše' + ? KNIHA_RECEPTU + : KNIHA_RECEPTU.filter(r => r.kategorie === categoryFilter); + + const results = []; + + source.forEach(r => { + const titleWords = tokenize(r.nazev); + const surWords = tokenize((r.suroviny || []).join(" ")); + const posWords = tokenize(r.postup || ""); + const katWords = tokenize(r.kategorie || ""); + + let score = 0; + let matchedTokens = 0; + let snippet = ""; + + tokens.forEach((tok, idx) => { + const st = stems[idx]; + let hit = false; + + // 1. Shoda v názvu (priorita #1) + if (titleWords.some(tw => wordMatches(tw, tok, st))) { + score += 150; + hit = true; + } + + // 2. Shoda v surovinách + if (surWords.some(sw => wordMatches(sw, tok, st))) { + score += 50; + hit = true; + if (!snippet) { + const found = (r.suroviny || []).find(s => tokenize(s).some(sw => wordMatches(sw, tok, st))); + if (found) snippet = "Surovina: " + found; + } + } + + // 3. Shoda v kategorii + if (katWords.some(kw => wordMatches(kw, tok, st))) { + score += 25; + hit = true; + } + + // 4. Shoda v postupu + if (posWords.some(pw => wordMatches(pw, tok, st))) { + score += 20; + hit = true; + if (!snippet && r.postup) { + snippet = "Postup: " + r.postup.substring(0, 110) + "..."; + } + } + + if (hit) matchedTokens++; + }); + + // Masivní bonus za pokrytí všech hledaných slov (AND logika) + if (matchedTokens === tokens.length) { + score += 500 * tokens.length; + } + + if (score > 0) { + results.push({ + recipe: r, + score: score, + matchedCount: matchedTokens, + snippet: snippet + }); + } + }); + + // Seřadit: nejdřív ty, co splňují všechny tokeny, pak podle skóre + results.sort((a, b) => { + const fullA = a.matchedCount === tokens.length ? 1 : 0; + const fullB = b.matchedCount === tokens.length ? 1 : 0; + if (fullA !== fullB) return fullB - fullA; + return b.score - a.score || a.recipe.id - b.recipe.id; + }); + + return results; } // Categories list @@ -66,14 +218,19 @@ const KATEGORIE = [ 'Moučníky a sladké', 'Přílohy a bezmasá', 'Omáčky', - 'Saláty a zelenina' + 'Saláty a zelenina', + 'Ostatní' ]; -// Views renderers +// ========================================================================= +// VIEWS & CONTROLLERS +// ========================================================================= + +// 1. HLAVNÍ NABÍDKA function renderMainMenu() { currentView = 'menu'; winTitleEl.textContent = "KUCHAŘKA KAMILY PEŠEK // HLAVNÍ NABÍDKA PASCAL 7.0"; - + let html = `