|
1 | | -use { |
2 | | - anyhow::{Context, Result}, |
3 | | - std::{fmt::Write as _, time::Instant}, |
4 | | - tracing::info, |
5 | | - wordbase::{Engine, Profile, RecordEntry, RecordKind, render::RenderConfig}, |
6 | | -}; |
| 1 | +use std::{fmt::Write, iter, time::Instant}; |
7 | 2 |
|
8 | | -pub fn deinflect(engine: &Engine, text: &str) { |
9 | | - for deinflect in engine.deinflect(text, 0) { |
10 | | - let text_part = text.get(deinflect.span).unwrap_or("(?)"); |
11 | | - info!("{text_part} -> {:?}", deinflect.lemma); |
12 | | - } |
13 | | -} |
| 3 | +use anyhow::Result; |
| 4 | +use itertools::Itertools; |
| 5 | +use tracing::info; |
| 6 | +use wordbase::{ |
| 7 | + DictionaryId, Engine, FrequencyValue, Profile, RecordEntry, RecordKind, |
| 8 | + dict::{self, jpn::PitchPosition}, |
| 9 | + dictionary::Dictionaries, |
| 10 | + render, |
| 11 | +}; |
14 | 12 |
|
15 | 13 | pub async fn lookup(engine: &Engine, profile: &Profile, text: &str) -> Result<Vec<RecordEntry>> { |
16 | 14 | let start = Instant::now(); |
17 | | - let records = engine.lookup(profile.id, text, 0, RecordKind::ALL).await?; |
| 15 | + let entries = engine.lookup(profile.id, text, 0, RecordKind::ALL).await?; |
18 | 16 | let end = Instant::now(); |
19 | | - // TODO: a nice, sort-of-human-readable output |
| 17 | + |
| 18 | + let dictionaries = engine.dictionaries(); |
| 19 | + let mut w = String::new(); |
| 20 | + for term in render::group_terms(&entries) { |
| 21 | + render(&mut w, &dictionaries, &term)?; |
| 22 | + } |
| 23 | + info!("\n{w}"); |
| 24 | + |
20 | 25 | info!("Fetched records in {:?}", end.duration_since(start)); |
21 | | - Ok(records) |
| 26 | + Ok(entries) |
22 | 27 | } |
23 | 28 |
|
24 | | -pub async fn lookup_lemma(engine: &Engine, profile: &Profile, lemma: &str) -> Result<()> { |
25 | | - for result in engine |
26 | | - .lookup_lemma(profile.id, &lemma, RecordKind::ALL) |
27 | | - .await? |
28 | | - { |
29 | | - println!("{result:#?}"); |
| 29 | +fn render(mut w: impl Write, dictionaries: &Dictionaries, term: &render::RecordTerm) -> Result<()> { |
| 30 | + if term.term.reading().is_some() { |
| 31 | + let reading = term |
| 32 | + .info |
| 33 | + .furigana_parts |
| 34 | + .iter() |
| 35 | + .map(|(headword, reading)| { |
| 36 | + if reading.is_empty() { |
| 37 | + headword.clone() |
| 38 | + } else { |
| 39 | + format!("{headword}[{reading}]") |
| 40 | + } |
| 41 | + }) |
| 42 | + .join(" "); |
| 43 | + writeln!(w, "{} - {reading}", term.term)?; |
| 44 | + } else { |
| 45 | + writeln!(w, "{}", term.term)?; |
30 | 46 | } |
| 47 | + |
| 48 | + let tags = iter::empty() |
| 49 | + .chain(tags_pitch(term)) |
| 50 | + .chain(tags_audio(dictionaries, term)) |
| 51 | + .chain(tags_frequency(dictionaries, term)) |
| 52 | + .map(|tag| format!("[{tag}]")) |
| 53 | + .join(" "); |
| 54 | + if !tags.is_empty() { |
| 55 | + writeln!(w, " {tags}\n")?; |
| 56 | + } |
| 57 | + |
| 58 | + for (&source, glossary_group) in &term.info.glossary_groups { |
| 59 | + writeln!(w, " {}:", dict_name(dictionaries, source))?; |
| 60 | + |
| 61 | + for glossary in glossary_group { |
| 62 | + for content in &glossary.content { |
| 63 | + writeln!(w, " {}", content.chars().take(40).collect::<String>())?; |
| 64 | + } |
| 65 | + } |
| 66 | + } |
| 67 | + |
| 68 | + writeln!(w, "\n")?; |
31 | 69 | Ok(()) |
32 | 70 | } |
33 | 71 |
|
34 | | -pub async fn render(engine: &Engine, profile: &Profile, text: &str) -> Result<()> { |
35 | | - let start = Instant::now(); |
36 | | - let records = engine.lookup(profile.id, text, 0, RecordKind::ALL).await?; |
37 | | - let end = Instant::now(); |
38 | | - info!("Fetched records in {:?}", end.duration_since(start)); |
| 72 | +fn tags_frequency<'a>( |
| 73 | + dictionaries: &'a Dictionaries, |
| 74 | + term: &'a render::RecordTerm, |
| 75 | +) -> impl Iterator<Item = String> + 'a { |
| 76 | + term.info |
| 77 | + .frequencies |
| 78 | + .iter() |
| 79 | + .map(move |(&source, frequencies)| { |
| 80 | + format!( |
| 81 | + "{} {}", |
| 82 | + dict_name(dictionaries, source), |
| 83 | + frequencies |
| 84 | + .iter() |
| 85 | + .map(|freq| render_frequency(freq)) |
| 86 | + .join("・") |
| 87 | + ) |
| 88 | + }) |
| 89 | +} |
39 | 90 |
|
40 | | - let start = Instant::now(); |
41 | | - let mut html = engine |
42 | | - .render_to_html( |
43 | | - &records, |
44 | | - &RenderConfig { |
45 | | - add_note_text: Some("Add Card".into()), |
46 | | - add_note_js_fn: Some("unimplemented".into()), |
47 | | - }, |
48 | | - ) |
49 | | - .context("failed to render HTML")?; |
50 | | - _ = write!(&mut html, "<style>{EXTRA_CSS}</style>"); |
51 | | - let end = Instant::now(); |
52 | | - info!("Rendered HTML in {:?}", end.duration_since(start)); |
| 91 | +fn render_frequency(frequency: &dict::yomitan::Frequency) -> String { |
| 92 | + match (&frequency.display, &frequency.value) { |
| 93 | + (Some(display), _) => display.clone(), |
| 94 | + (None, Some(FrequencyValue::Rank(rank))) => format!("{rank} ↓"), |
| 95 | + (None, Some(FrequencyValue::Occurrence(occurrence))) => format!("{occurrence} ↑"), |
| 96 | + (None, None) => "?".into(), |
| 97 | + } |
| 98 | +} |
53 | 99 |
|
54 | | - println!("{html}"); |
55 | | - Ok(()) |
| 100 | +fn tags_pitch(term: &render::RecordTerm) -> impl Iterator<Item = String> { |
| 101 | + let morae = term |
| 102 | + .term |
| 103 | + .reading() |
| 104 | + .map(|s| dict::jpn::morae(s).collect::<Vec<_>>()); |
| 105 | + |
| 106 | + term.info |
| 107 | + .pitches |
| 108 | + .iter() |
| 109 | + .map(|(position, pitch)| render_pitch(morae.as_deref(), *position, pitch)) |
| 110 | + .collect::<Vec<_>>() |
| 111 | + .into_iter() |
56 | 112 | } |
57 | 113 |
|
58 | | -// TODO: this should probably be put into the renderer somehow |
59 | | -const EXTRA_CSS: &str = " |
60 | | -:root { |
61 | | - --accent-color: #3584e4; |
62 | | - --on-accent-color: #ffffff; |
| 114 | +fn render_pitch(morae: Option<&[&str]>, position: PitchPosition, pitch: &render::Pitch) -> String { |
| 115 | + let reading = (|| { |
| 116 | + let mut morae = morae?.iter().zip(pitch.high.iter()).peekable(); |
| 117 | + let mut last_high = *morae.peek()?.1; |
| 118 | + let mut reading = String::new(); |
| 119 | + for (mora, &high) in morae { |
| 120 | + match (last_high, high) { |
| 121 | + (false, false) | (true, true) => {} |
| 122 | + (false, true) => { |
| 123 | + _ = write!(reading, "/"); |
| 124 | + } |
| 125 | + (true, false) => { |
| 126 | + _ = write!(reading, "\"); |
| 127 | + } |
| 128 | + } |
| 129 | + last_high = high; |
| 130 | + _ = write!(reading, "{mora}"); |
| 131 | + } |
| 132 | + Some(reading) |
| 133 | + })(); |
| 134 | + |
| 135 | + let reading = reading.unwrap_or_else(|| format!("{}", position.0)); |
| 136 | + iter::once(reading) |
| 137 | + .chain((0..pitch.audio.len()).map(|_| "🔊".to_string())) |
| 138 | + .join(" ") |
63 | 139 | } |
64 | 140 |
|
65 | | -:root { |
66 | | - --bg-color: #fafafb; |
67 | | - --fg-color: rgb(0 0 6 / 80%); |
| 141 | +fn tags_audio( |
| 142 | + dictionaries: &Dictionaries, |
| 143 | + term: &render::RecordTerm, |
| 144 | +) -> impl Iterator<Item = String> { |
| 145 | + term.info |
| 146 | + .audio_no_pitch |
| 147 | + .iter() |
| 148 | + .map(|(&source, _)| format!("🔊 {}", dict_name(dictionaries, source))) |
68 | 149 | } |
69 | 150 |
|
70 | | -@media (prefers-color-scheme: dark) { |
71 | | - :root { |
72 | | - --bg-color: #222226; |
73 | | - --fg-color: #ffffff; |
74 | | - } |
| 151 | +fn dict_name(dictionaries: &Dictionaries, id: DictionaryId) -> &str { |
| 152 | + dictionaries |
| 153 | + .get(&id) |
| 154 | + .map_or("?", |dict| dict.meta.name.as_str()) |
75 | 155 | } |
76 | | -"; |
|
0 commit comments