auto-furigana search

This commit is contained in:
polv committed 2026-02-23 18:37:57 +07:00
1 parent 1e91f21ca4
commit 37c3adabfc
3 files changed
+95 -10

No files matched your search

+6 -3
View File
@@ -53,7 +53,7 @@ impl YomitanDatabase {
LEFT JOIN yomitan.def_tag_sets dt ON dt.id = t.def_tags_id 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.rule_sets r ON r.id = t.rules_id
LEFT JOIN yomitan.term_tag_sets tt ON tt.id = t.term_tags_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 ORDER BY t.score DESC
LIMIT ?3 OFFSET ?4 LIMIT ?3 OFFSET ?4
"#, "#,
@@ -63,7 +63,9 @@ impl YomitanDatabase {
let conn = self.db.conn.lock().unwrap(); let conn = self.db.conn.lock().unwrap();
let mut stmt = conn.prepare(&sql)?; let mut stmt = conn.prepare(&sql)?;
let rows = stmt.query_map(params![q_term, q_reading, limit, offset], |r| { let rows = stmt.query_map(
params![q_term.to_ascii_lowercase(), q_reading, limit, offset],
|r| {
Ok(YomitanRow { Ok(YomitanRow {
term: r.get(0)?, term: r.get(0)?,
reading: r.get(1)?, reading: r.get(1)?,
@@ -75,7 +77,8 @@ impl YomitanDatabase {
term_tags: r.get(7)?, term_tags: r.get(7)?,
dict_title: r.get(8)?, dict_title: r.get(8)?,
}) })
})?; },
)?;
let mut out = Vec::new(); let mut out = Vec::new();
for r in rows { for r in rows {
+1 -1
View File
@@ -113,7 +113,7 @@ impl YomitanWriter {
CREATE INDEX IF NOT EXISTS dictionaries_lang ON dictionaries (lang); 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_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 terms_reading ON terms (reading);
CREATE INDEX IF NOT EXISTS term_meta_dict_id ON term_meta (dict_id); CREATE INDEX IF NOT EXISTS term_meta_dict_id ON term_meta (dict_id);
+88 -6
View File
@@ -1,6 +1,6 @@
import "./App.css"; 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 { Box, Button, Card, Group, Input, Stack } from "@chakra-ui/react";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
@@ -11,21 +11,56 @@ import { Provider } from "./components/ui/provider";
function App() { function App() {
const [q, setQ] = useState(""); const [q, setQ] = useState("");
const [entries, setEntries] = useState<any[]>([]); const [entries, setEntries] = useState<any[]>([]);
const [searchTimeout, setSearchTimeout] = useState(0);
const [furigana, setFurigana] = useState("");
useEffect(() => { useEffect(() => {
doSearch(); trySearch();
}, [q]); }, [q]);
async function doSearch() { async function trySearch() {
if (!q.trim()) { if (!q.trim()) {
setEntries([]); setEntries([]);
return; return;
} }
const ender = q.endsWith(" ") ? "" : "*"; if (searchTimeout) {
clearTimeout(searchTimeout);
}
const qTerm = q.trim() + ender; setSearchTimeout(
const qReading = q.trim() + ender; 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( setEntries(
await invoke("search_yomitan", { 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 ( return (
<Provider> <Provider>
<Stack maxW={"1000px"} margin={"0.5em auto"}> <Stack maxW={"1000px"} margin={"0.5em auto"}>
@@ -50,6 +130,8 @@ function App() {
id="greet-input" id="greet-input"
value={q} value={q}
onChange={(e) => setQ(e.currentTarget.value)} onChange={(e) => setQ(e.currentTarget.value)}
onCompositionUpdate={updateFurigana}
onCompositionEnd={addFurigana}
autoComplete="off" autoComplete="off"
spellCheck={false} spellCheck={false}
placeholder="Search..." placeholder="Search..."