diff --git a/src-core/src/db/yomitan.rs b/src-core/src/db/yomitan.rs index d88854c..8c642ea 100644 --- a/src-core/src/db/yomitan.rs +++ b/src-core/src/db/yomitan.rs @@ -53,7 +53,7 @@ impl YomitanDatabase { LEFT JOIN yomitan.def_tag_sets dt ON dt.id = t.def_tags_id LEFT JOIN yomitan.rule_sets r ON r.id = t.rules_id LEFT JOIN yomitan.term_tag_sets tt ON tt.id = t.term_tags_id - WHERE t.term GLOB ?1 {} t.reading GLOB ?2 + WHERE LOWER(t.term) GLOB ?1 {} t.reading GLOB ?2 ORDER BY t.score DESC LIMIT ?3 OFFSET ?4 "#, @@ -63,19 +63,22 @@ impl YomitanDatabase { let conn = self.db.conn.lock().unwrap(); let mut stmt = conn.prepare(&sql)?; - let rows = stmt.query_map(params![q_term, q_reading, limit, offset], |r| { - Ok(YomitanRow { - term: r.get(0)?, - reading: r.get(1)?, - def_tags: r.get(2)?, - rules: r.get(3)?, - score: r.get(4)?, - glossary_json: r.get(5)?, - sequence: r.get(6)?, - term_tags: r.get(7)?, - dict_title: r.get(8)?, - }) - })?; + let rows = stmt.query_map( + params![q_term.to_ascii_lowercase(), q_reading, limit, offset], + |r| { + Ok(YomitanRow { + term: r.get(0)?, + reading: r.get(1)?, + def_tags: r.get(2)?, + rules: r.get(3)?, + score: r.get(4)?, + glossary_json: r.get(5)?, + sequence: r.get(6)?, + term_tags: r.get(7)?, + dict_title: r.get(8)?, + }) + }, + )?; let mut out = Vec::new(); for r in rows { diff --git a/src-core/src/db/yomitan_writer.rs b/src-core/src/db/yomitan_writer.rs index cd3c66f..06ca771 100644 --- a/src-core/src/db/yomitan_writer.rs +++ b/src-core/src/db/yomitan_writer.rs @@ -113,7 +113,7 @@ impl YomitanWriter { CREATE INDEX IF NOT EXISTS dictionaries_lang ON dictionaries (lang); CREATE INDEX IF NOT EXISTS terms_dict_id ON terms (dict_id); - CREATE INDEX IF NOT EXISTS terms_term ON terms (term); + CREATE INDEX IF NOT EXISTS terms_term_LOWER ON terms (LOWER(term)); CREATE INDEX IF NOT EXISTS terms_reading ON terms (reading); CREATE INDEX IF NOT EXISTS term_meta_dict_id ON term_meta (dict_id); diff --git a/src-react/src/App.tsx b/src-react/src/App.tsx index 7fae92b..2fd5388 100644 --- a/src-react/src/App.tsx +++ b/src-react/src/App.tsx @@ -1,6 +1,6 @@ import "./App.css"; -import { useEffect, useState } from "react"; +import { CompositionEventHandler, useEffect, useState } from "react"; import { Box, Button, Card, Group, Input, Stack } from "@chakra-ui/react"; import { invoke } from "@tauri-apps/api/core"; @@ -11,21 +11,56 @@ import { Provider } from "./components/ui/provider"; function App() { const [q, setQ] = useState(""); const [entries, setEntries] = useState([]); + const [searchTimeout, setSearchTimeout] = useState(0); + const [furigana, setFurigana] = useState(""); useEffect(() => { - doSearch(); + trySearch(); }, [q]); - async function doSearch() { + async function trySearch() { if (!q.trim()) { setEntries([]); return; } - const ender = q.endsWith(" ") ? "" : "*"; + if (searchTimeout) { + clearTimeout(searchTimeout); + } - const qTerm = q.trim() + ender; - const qReading = q.trim() + ender; + setSearchTimeout( + setTimeout(() => { + runSearch(); + }, 250), + ); + } + + async function runSearch() { + const ender = /\p{Z}$/u.test(q) ? "" : "*"; + + const re = /\<(.+?)\>\[(.+?)\]/g; + + let qTerm = ""; + let qReading = ""; + + q.trim() + .split(re) + .map((s, i) => { + switch (i % 3) { + case 1: + qTerm += s; + break; + case 2: + qReading += s; + break; + default: + qTerm += s; + qReading += s; + } + }); + + qTerm += ender; + qReading += ender; setEntries( await invoke("search_yomitan", { @@ -37,6 +72,51 @@ function App() { ); } + const FURIGANA_REGEX = /^[\p{scx=Hiragana}\p{scx=Katakana}]+$/u; + const KANJI_REGEX = /([\p{sc=Han}\p{N}々〆ヵヶ]+)/u; + + const updateFurigana: CompositionEventHandler = ({ + data: compositionData, + }) => { + if (FURIGANA_REGEX.test(compositionData)) { + setFurigana(compositionData); + } + }; + + const addFurigana: CompositionEventHandler = ({ data: compositionData }) => { + const cleanedFuri = [...furigana.replace(/n/g, "ん")] + .map((c) => katakanaToHiragana(c)) + .join(""); + + let parts = compositionData.split(KANJI_REGEX); + if (parts.length === 1) return; + + const hiraganaParts = parts.map((p) => + [...p].map((c) => katakanaToHiragana(c)).join(""), + ); + const regex = new RegExp( + `^${hiraganaParts.map((p, idx) => `(${idx & 1 ? ".+" : p})`).join("")}$`, + ); + let rt: (string | null)[] = furigana.match(regex) || []; + if (!rt.length) { + parts = [compositionData]; + rt = [null, cleanedFuri]; + } + rt.shift(); + + const markup = parts + .map((p, idx) => (idx & 1 ? `<${p}>[${rt[idx]}]` : p)) + .join(""); + setQ(markup); + }; + + function katakanaToHiragana(k: string) { + let c = k.charCodeAt(0); + return c >= 12449 && c <= 12531 + ? String.fromCharCode(k.charCodeAt(0) - 96) + : k; + } + return ( @@ -50,6 +130,8 @@ function App() { id="greet-input" value={q} onChange={(e) => setQ(e.currentTarget.value)} + onCompositionUpdate={updateFurigana} + onCompositionEnd={addFurigana} autoComplete="off" spellCheck={false} placeholder="Search..."