reading
This commit is contained in:
@@ -0,0 +1,831 @@
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Word {
|
||||
pub id: &'static str,
|
||||
pub spelling: &'static str,
|
||||
pub ipa_us: &'static str,
|
||||
pub russian_hint: &'static str,
|
||||
pub audio_path: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Token {
|
||||
Word {
|
||||
word_id: &'static str,
|
||||
surface: &'static str,
|
||||
},
|
||||
Punctuation(&'static str),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct ReadingText {
|
||||
pub id: &'static str,
|
||||
pub title: &'static str,
|
||||
pub tokens: &'static [Token],
|
||||
}
|
||||
|
||||
macro_rules! entry {
|
||||
($id:literal, $ipa:literal, $hint:literal) => {
|
||||
Word {
|
||||
id: $id,
|
||||
spelling: $id,
|
||||
ipa_us: $ipa,
|
||||
russian_hint: $hint,
|
||||
audio_path: concat!("/audio/", $id, ".mp3"),
|
||||
}
|
||||
};
|
||||
}
|
||||
macro_rules! word {
|
||||
($id:literal) => {
|
||||
Token::Word {
|
||||
word_id: $id,
|
||||
surface: $id,
|
||||
}
|
||||
};
|
||||
($id:literal, $surface:literal) => {
|
||||
Token::Word {
|
||||
word_id: $id,
|
||||
surface: $surface,
|
||||
}
|
||||
};
|
||||
}
|
||||
macro_rules! punct {
|
||||
($value:literal) => {
|
||||
Token::Punctuation($value)
|
||||
};
|
||||
}
|
||||
|
||||
pub const DICTIONARY: &[Word] = &[
|
||||
entry!("sam", "sæm", "сэм"),
|
||||
entry!("and", "ænd", "энд"),
|
||||
entry!("ben", "bɛn", "бэн"),
|
||||
entry!("sit", "sɪt", "сит"),
|
||||
entry!("at", "æt", "эт"),
|
||||
entry!("camp", "kæmp", "кэмп"),
|
||||
entry!("plan", "plæn", "плэн"),
|
||||
entry!("next", "nɛkst", "нэкст"),
|
||||
entry!("trip", "trɪp", "трип"),
|
||||
entry!("tim", "tɪm", "тим"),
|
||||
entry!("gus", "ɡʌs", "гас"),
|
||||
entry!("pack", "pæk", "пэк"),
|
||||
entry!("red", "rɛd", "рэд"),
|
||||
entry!("bags", "bæɡz", "бэгз"),
|
||||
entry!("fresh", "frɛʃ", "фреш"),
|
||||
entry!("fish", "fɪʃ", "фиш"),
|
||||
entry!("chips", "tʃɪps", "чипс"),
|
||||
entry!("has", "hæz", "хэз"),
|
||||
entry!("his", "hɪz", "хиз"),
|
||||
entry!("map", "mæp", "мэп"),
|
||||
entry!("big", "bɪɡ", "биг"),
|
||||
entry!("maps", "mæps", "мэпс"),
|
||||
entry!("black", "blæk", "блэк"),
|
||||
entry!("in", "ɪn", "ин"),
|
||||
entry!("sits", "sɪts", "ситс"),
|
||||
entry!("past", "pæst", "пэст"),
|
||||
entry!("hills", "hɪlz", "хилз"),
|
||||
entry!("wide", "waɪd", "уайд"),
|
||||
entry!("grass", "ɡræs", "грэс"),
|
||||
entry!("trails", "treɪlz", "трейлз"),
|
||||
entry!("rain", "reɪn", "рейн"),
|
||||
entry!("hits", "hɪts", "хитс"),
|
||||
entry!("runs", "rʌnz", "ранз"),
|
||||
entry!("rocks", "rɑks", "ракс"),
|
||||
entry!("wait", "weɪt", "уэйт"),
|
||||
entry!("tents", "tɛnts", "тентс"),
|
||||
entry!("chat", "tʃæt", "чэт"),
|
||||
entry!("can", "kæn", "кэн"),
|
||||
entry!("see", "siː", "си"),
|
||||
entry!("drip", "drɪp", "дрип"),
|
||||
entry!("on", "ɑn", "ан"),
|
||||
entry!("fast", "fæst", "фэст"),
|
||||
entry!("swim", "swɪm", "свим"),
|
||||
entry!("pond", "pɑnd", "панд"),
|
||||
entry!("grab", "ɡræb", "грэб"),
|
||||
entry!("rush", "rʌʃ", "раш"),
|
||||
entry!("wet", "wɛt", "уэт"),
|
||||
entry!("make", "meɪk", "мейк"),
|
||||
entry!("bright", "braɪt", "брайт"),
|
||||
entry!("light", "laɪt", "лайт"),
|
||||
entry!("dry", "draɪ", "драй"),
|
||||
entry!("sticks", "stɪks", "стикс"),
|
||||
entry!("help", "hɛlp", "хэлп"),
|
||||
entry!("chip", "tʃɪp", "чип"),
|
||||
entry!("dish", "dɪʃ", "диш"),
|
||||
entry!("ends", "ɛndz", "эндз"),
|
||||
entry!("sun", "sʌn", "сан"),
|
||||
entry!("shine", "ʃaɪn", "шайн"),
|
||||
entry!("hike", "haɪk", "хайк"),
|
||||
entry!("ride", "raɪd", "райд"),
|
||||
entry!("bikes", "baɪks", "байкс"),
|
||||
entry!("pick", "pɪk", "пик"),
|
||||
entry!("coins", "kɔɪnz", "койнз"),
|
||||
entry!("sand", "sænd", "сэнд"),
|
||||
entry!("by", "baɪ", "бай"),
|
||||
entry!("road", "roʊd", "роуд"),
|
||||
entry!("shells", "ʃɛlz", "шелз"),
|
||||
entry!("wash", "wɑʃ", "уош"),
|
||||
entry!("chop", "tʃɑp", "чап"),
|
||||
entry!("fix", "fɪks", "фикс"),
|
||||
entry!("bike", "baɪk", "байк"),
|
||||
entry!("tape", "teɪp", "тейп"),
|
||||
entry!("night", "naɪt", "найт"),
|
||||
entry!("fills", "fɪlz", "филз"),
|
||||
entry!("sky", "skaɪ", "скай"),
|
||||
entry!("dims", "dɪmz", "димз"),
|
||||
entry!("sing", "sɪŋ", "синг"),
|
||||
entry!("grin", "ɡrɪn", "грин"),
|
||||
entry!("may", "meɪ", "мей"),
|
||||
entry!("hit", "hɪt", "хит"),
|
||||
entry!("but", "bʌt", "бат"),
|
||||
entry!("lamps", "læmps", "лэмпс"),
|
||||
entry!("keep", "kiːp", "кип"),
|
||||
entry!("tent", "tɛnt", "тент"),
|
||||
entry!("safe", "seɪf", "сейф"),
|
||||
entry!("bag", "bæɡ", "бэг"),
|
||||
entry!("day", "deɪ", "дей"),
|
||||
entry!("stops", "stɑps", "стапс"),
|
||||
entry!("spot", "spɑt", "спат"),
|
||||
entry!("hot", "hɑt", "хат"),
|
||||
entry!("last", "læst", "лэст"),
|
||||
entry!("up", "ʌp", "ап"),
|
||||
entry!("home", "hoʊm", "хоум"),
|
||||
entry!("dusk", "dʌsk", "даск"),
|
||||
entry!("end", "ɛnd", "энд"),
|
||||
entry!("lights", "laɪts", "лайтс"),
|
||||
entry!("wave", "weɪv", "уэйв"),
|
||||
];
|
||||
|
||||
const BASIC_RULES_TOKENS: &[Token] = &[
|
||||
word!("sam", "Sam"),
|
||||
word!("and"),
|
||||
word!("ben", "Ben"),
|
||||
word!("sit"),
|
||||
word!("at"),
|
||||
word!("camp"),
|
||||
word!("and"),
|
||||
word!("plan"),
|
||||
word!("next"),
|
||||
word!("trip"),
|
||||
punct!("."),
|
||||
word!("tim", "Tim"),
|
||||
word!("and"),
|
||||
word!("gus", "Gus"),
|
||||
word!("pack"),
|
||||
word!("red"),
|
||||
word!("bags"),
|
||||
word!("fresh"),
|
||||
word!("fish"),
|
||||
word!("and"),
|
||||
word!("chips"),
|
||||
punct!("."),
|
||||
word!("sam", "Sam"),
|
||||
word!("has"),
|
||||
word!("his"),
|
||||
word!("map"),
|
||||
word!("and"),
|
||||
word!("ben", "Ben"),
|
||||
word!("has"),
|
||||
word!("his"),
|
||||
word!("big"),
|
||||
word!("map"),
|
||||
punct!("."),
|
||||
word!("tim", "Tim"),
|
||||
word!("has"),
|
||||
word!("red"),
|
||||
word!("maps"),
|
||||
punct!(";"),
|
||||
word!("gus", "Gus"),
|
||||
word!("has"),
|
||||
word!("black"),
|
||||
word!("maps"),
|
||||
word!("in"),
|
||||
word!("camp"),
|
||||
punct!("."),
|
||||
word!("camp", "Camp"),
|
||||
word!("sits"),
|
||||
word!("past"),
|
||||
word!("big"),
|
||||
word!("hills"),
|
||||
word!("and"),
|
||||
word!("wide"),
|
||||
word!("camp"),
|
||||
word!("grass"),
|
||||
word!("trails"),
|
||||
punct!("."),
|
||||
word!("rain", "Rain"),
|
||||
word!("hits"),
|
||||
word!("grass"),
|
||||
word!("and"),
|
||||
word!("runs"),
|
||||
word!("past"),
|
||||
word!("black"),
|
||||
word!("rocks"),
|
||||
word!("in"),
|
||||
word!("camp"),
|
||||
punct!("."),
|
||||
word!("sam", "Sam"),
|
||||
punct!(","),
|
||||
word!("ben", "Ben"),
|
||||
punct!(","),
|
||||
word!("tim", "Tim"),
|
||||
punct!(","),
|
||||
word!("and"),
|
||||
word!("gus", "Gus"),
|
||||
word!("wait"),
|
||||
word!("in"),
|
||||
word!("tents"),
|
||||
word!("and"),
|
||||
word!("chat"),
|
||||
punct!("."),
|
||||
word!("gus", "Gus"),
|
||||
word!("can"),
|
||||
word!("see"),
|
||||
word!("rain"),
|
||||
word!("drip"),
|
||||
word!("on"),
|
||||
word!("red"),
|
||||
word!("camp"),
|
||||
word!("tents"),
|
||||
word!("fast"),
|
||||
punct!("."),
|
||||
word!("ben", "Ben"),
|
||||
word!("can"),
|
||||
word!("see"),
|
||||
word!("fish"),
|
||||
word!("swim"),
|
||||
word!("in"),
|
||||
word!("big"),
|
||||
word!("camp"),
|
||||
word!("pond"),
|
||||
word!("fast"),
|
||||
punct!("."),
|
||||
word!("tim", "Tim"),
|
||||
word!("and"),
|
||||
word!("gus", "Gus"),
|
||||
word!("grab"),
|
||||
word!("bags"),
|
||||
word!("and"),
|
||||
word!("rush"),
|
||||
word!("past"),
|
||||
word!("wet"),
|
||||
word!("grass"),
|
||||
punct!("."),
|
||||
word!("sam", "Sam"),
|
||||
word!("can"),
|
||||
word!("make"),
|
||||
word!("bright"),
|
||||
word!("camp"),
|
||||
word!("light"),
|
||||
punct!(";"),
|
||||
word!("dry"),
|
||||
word!("sticks"),
|
||||
word!("help"),
|
||||
word!("sam", "Sam"),
|
||||
punct!("."),
|
||||
word!("ben", "Ben"),
|
||||
word!("can"),
|
||||
word!("make"),
|
||||
word!("fresh"),
|
||||
word!("fish"),
|
||||
word!("chip"),
|
||||
word!("dish"),
|
||||
word!("at"),
|
||||
word!("camp"),
|
||||
word!("tent"),
|
||||
punct!("."),
|
||||
word!("rain", "Rain"),
|
||||
word!("ends"),
|
||||
word!("and"),
|
||||
word!("sun"),
|
||||
word!("can"),
|
||||
word!("shine"),
|
||||
word!("on"),
|
||||
word!("hills"),
|
||||
word!("at"),
|
||||
word!("camp"),
|
||||
punct!("."),
|
||||
word!("sam", "Sam"),
|
||||
word!("and"),
|
||||
word!("ben", "Ben"),
|
||||
word!("hike"),
|
||||
word!("past"),
|
||||
word!("big"),
|
||||
word!("rocks"),
|
||||
word!("in"),
|
||||
word!("rain"),
|
||||
word!("fast"),
|
||||
punct!("."),
|
||||
word!("tim", "Tim"),
|
||||
word!("and"),
|
||||
word!("gus", "Gus"),
|
||||
word!("ride"),
|
||||
word!("bikes"),
|
||||
word!("on"),
|
||||
word!("wide"),
|
||||
word!("camp"),
|
||||
word!("grass"),
|
||||
word!("trails"),
|
||||
punct!("."),
|
||||
word!("sam", "Sam"),
|
||||
word!("can"),
|
||||
word!("pick"),
|
||||
word!("coins"),
|
||||
word!("in"),
|
||||
word!("sand"),
|
||||
word!("by"),
|
||||
word!("wide"),
|
||||
word!("camp"),
|
||||
word!("road"),
|
||||
punct!("."),
|
||||
word!("ben", "Ben"),
|
||||
word!("can"),
|
||||
word!("pick"),
|
||||
word!("bright"),
|
||||
word!("shells"),
|
||||
word!("in"),
|
||||
word!("sand"),
|
||||
word!("by"),
|
||||
word!("wide"),
|
||||
word!("road"),
|
||||
punct!("."),
|
||||
word!("tim", "Tim"),
|
||||
word!("can"),
|
||||
word!("wash"),
|
||||
word!("fish"),
|
||||
word!("and"),
|
||||
word!("chop"),
|
||||
word!("red"),
|
||||
word!("chips"),
|
||||
word!("at"),
|
||||
word!("camp"),
|
||||
punct!("."),
|
||||
word!("gus", "Gus"),
|
||||
word!("can"),
|
||||
word!("fix"),
|
||||
word!("his"),
|
||||
word!("bike"),
|
||||
punct!(";"),
|
||||
word!("black"),
|
||||
word!("tape"),
|
||||
word!("can"),
|
||||
word!("help"),
|
||||
word!("gus", "Gus"),
|
||||
punct!("."),
|
||||
word!("sam", "Sam"),
|
||||
word!("and"),
|
||||
word!("ben", "Ben"),
|
||||
word!("help"),
|
||||
word!("gus", "Gus"),
|
||||
word!("fix"),
|
||||
word!("his"),
|
||||
word!("bike"),
|
||||
word!("at"),
|
||||
word!("camp"),
|
||||
punct!("."),
|
||||
word!("night", "Night"),
|
||||
word!("fills"),
|
||||
word!("camp"),
|
||||
word!("and"),
|
||||
word!("black"),
|
||||
word!("sky"),
|
||||
word!("dims"),
|
||||
word!("camp"),
|
||||
word!("trails"),
|
||||
word!("fast"),
|
||||
punct!("."),
|
||||
word!("sam", "Sam"),
|
||||
word!("and"),
|
||||
word!("ben", "Ben"),
|
||||
word!("sit"),
|
||||
word!("by"),
|
||||
word!("bright"),
|
||||
word!("camp"),
|
||||
word!("light"),
|
||||
word!("and"),
|
||||
word!("sing"),
|
||||
punct!("."),
|
||||
word!("tim", "Tim"),
|
||||
word!("and"),
|
||||
word!("gus", "Gus"),
|
||||
word!("sit"),
|
||||
word!("by"),
|
||||
word!("bright"),
|
||||
word!("camp"),
|
||||
word!("light"),
|
||||
word!("and"),
|
||||
word!("grin"),
|
||||
punct!("."),
|
||||
word!("rain", "Rain"),
|
||||
word!("may"),
|
||||
word!("hit"),
|
||||
word!("tents"),
|
||||
word!("but"),
|
||||
word!("camp"),
|
||||
word!("lamps"),
|
||||
word!("make"),
|
||||
word!("bright"),
|
||||
word!("light"),
|
||||
punct!("."),
|
||||
word!("sam", "Sam"),
|
||||
word!("can"),
|
||||
word!("keep"),
|
||||
word!("dry"),
|
||||
word!("bags"),
|
||||
word!("in"),
|
||||
word!("his"),
|
||||
word!("tent"),
|
||||
word!("at"),
|
||||
word!("camp"),
|
||||
punct!("."),
|
||||
word!("ben", "Ben"),
|
||||
word!("can"),
|
||||
word!("keep"),
|
||||
word!("his"),
|
||||
word!("map"),
|
||||
word!("safe"),
|
||||
word!("in"),
|
||||
word!("tent"),
|
||||
word!("at"),
|
||||
word!("camp"),
|
||||
punct!("."),
|
||||
word!("tim", "Tim"),
|
||||
word!("can"),
|
||||
word!("keep"),
|
||||
word!("fish"),
|
||||
word!("fresh"),
|
||||
word!("in"),
|
||||
word!("his"),
|
||||
word!("bag"),
|
||||
word!("at"),
|
||||
word!("camp"),
|
||||
punct!("."),
|
||||
word!("gus", "Gus"),
|
||||
word!("can"),
|
||||
word!("keep"),
|
||||
word!("red"),
|
||||
word!("chips"),
|
||||
word!("in"),
|
||||
word!("his"),
|
||||
word!("bag"),
|
||||
word!("at"),
|
||||
word!("camp"),
|
||||
punct!("."),
|
||||
word!("day", "Day"),
|
||||
word!("ends"),
|
||||
punct!(";"),
|
||||
word!("rain"),
|
||||
word!("stops"),
|
||||
punct!("."),
|
||||
word!("sun", "Sun"),
|
||||
word!("can"),
|
||||
word!("shine"),
|
||||
word!("at"),
|
||||
word!("red"),
|
||||
word!("camp"),
|
||||
punct!("."),
|
||||
word!("sam", "Sam"),
|
||||
word!("and"),
|
||||
word!("ben", "Ben"),
|
||||
word!("plan"),
|
||||
word!("next"),
|
||||
word!("hike"),
|
||||
word!("by"),
|
||||
word!("wide"),
|
||||
word!("camp"),
|
||||
word!("road"),
|
||||
punct!("."),
|
||||
word!("tim", "Tim"),
|
||||
word!("and"),
|
||||
word!("gus", "Gus"),
|
||||
word!("plan"),
|
||||
word!("next"),
|
||||
word!("bike"),
|
||||
word!("ride"),
|
||||
word!("by"),
|
||||
word!("camp"),
|
||||
word!("road"),
|
||||
punct!("."),
|
||||
word!("sam", "Sam"),
|
||||
word!("can"),
|
||||
word!("help"),
|
||||
word!("ben", "Ben"),
|
||||
word!("past"),
|
||||
word!("rocks"),
|
||||
word!("and"),
|
||||
word!("wet"),
|
||||
word!("camp"),
|
||||
word!("grass"),
|
||||
punct!("."),
|
||||
word!("tim", "Tim"),
|
||||
word!("can"),
|
||||
word!("help"),
|
||||
word!("gus", "Gus"),
|
||||
word!("past"),
|
||||
word!("hills"),
|
||||
word!("and"),
|
||||
word!("red"),
|
||||
word!("camp"),
|
||||
word!("tents"),
|
||||
punct!("."),
|
||||
word!("ben", "Ben"),
|
||||
word!("can"),
|
||||
word!("ride"),
|
||||
word!("bike"),
|
||||
word!("past"),
|
||||
word!("camp"),
|
||||
word!("pond"),
|
||||
word!("and"),
|
||||
word!("rocks"),
|
||||
word!("fast"),
|
||||
punct!("."),
|
||||
word!("gus", "Gus"),
|
||||
word!("can"),
|
||||
word!("ride"),
|
||||
word!("bike"),
|
||||
word!("past"),
|
||||
word!("wet"),
|
||||
word!("grass"),
|
||||
word!("and"),
|
||||
word!("tents"),
|
||||
word!("fast"),
|
||||
punct!("."),
|
||||
word!("sam", "Sam"),
|
||||
word!("can"),
|
||||
word!("see"),
|
||||
word!("bright"),
|
||||
word!("light"),
|
||||
word!("on"),
|
||||
word!("camp"),
|
||||
word!("road"),
|
||||
word!("at"),
|
||||
word!("night"),
|
||||
punct!("."),
|
||||
word!("ben", "Ben"),
|
||||
word!("can"),
|
||||
word!("see"),
|
||||
word!("bright"),
|
||||
word!("lights"),
|
||||
word!("at"),
|
||||
word!("night"),
|
||||
word!("in"),
|
||||
word!("his"),
|
||||
word!("camp"),
|
||||
punct!("."),
|
||||
word!("tim", "Tim"),
|
||||
word!("can"),
|
||||
word!("spot"),
|
||||
word!("red"),
|
||||
word!("fish"),
|
||||
word!("in"),
|
||||
word!("pond"),
|
||||
word!("at"),
|
||||
word!("night"),
|
||||
word!("fast"),
|
||||
punct!("."),
|
||||
word!("gus", "Gus"),
|
||||
word!("can"),
|
||||
word!("spot"),
|
||||
word!("big"),
|
||||
word!("fish"),
|
||||
word!("in"),
|
||||
word!("pond"),
|
||||
word!("at"),
|
||||
word!("night"),
|
||||
word!("fast"),
|
||||
punct!("."),
|
||||
word!("sam", "Sam"),
|
||||
word!("and"),
|
||||
word!("ben", "Ben"),
|
||||
word!("make"),
|
||||
word!("hot"),
|
||||
word!("fish"),
|
||||
word!("dish"),
|
||||
word!("in"),
|
||||
word!("camp"),
|
||||
word!("tent"),
|
||||
punct!("."),
|
||||
word!("tim", "Tim"),
|
||||
word!("and"),
|
||||
word!("gus", "Gus"),
|
||||
word!("make"),
|
||||
word!("fresh"),
|
||||
word!("chip"),
|
||||
word!("dish"),
|
||||
word!("in"),
|
||||
word!("camp"),
|
||||
word!("tent"),
|
||||
punct!("."),
|
||||
word!("rain", "Rain"),
|
||||
word!("ends"),
|
||||
word!("and"),
|
||||
word!("camp"),
|
||||
word!("trails"),
|
||||
word!("dry"),
|
||||
word!("in"),
|
||||
word!("sun"),
|
||||
word!("at"),
|
||||
word!("last"),
|
||||
punct!("."),
|
||||
word!("sam", "Sam"),
|
||||
word!("and"),
|
||||
word!("ben", "Ben"),
|
||||
word!("pick"),
|
||||
word!("wet"),
|
||||
word!("tents"),
|
||||
word!("up"),
|
||||
word!("and"),
|
||||
word!("pack"),
|
||||
word!("fast"),
|
||||
punct!("."),
|
||||
word!("tim", "Tim"),
|
||||
word!("and"),
|
||||
word!("gus", "Gus"),
|
||||
word!("pack"),
|
||||
word!("fish"),
|
||||
word!("chips"),
|
||||
word!("maps"),
|
||||
word!("and"),
|
||||
word!("bags"),
|
||||
word!("fast"),
|
||||
punct!("."),
|
||||
word!("sam", "Sam"),
|
||||
word!("and"),
|
||||
word!("ben", "Ben"),
|
||||
word!("hike"),
|
||||
word!("home"),
|
||||
word!("by"),
|
||||
word!("wide"),
|
||||
word!("road"),
|
||||
word!("at"),
|
||||
word!("dusk"),
|
||||
punct!("."),
|
||||
word!("tim", "Tim"),
|
||||
word!("and"),
|
||||
word!("gus", "Gus"),
|
||||
word!("ride"),
|
||||
word!("home"),
|
||||
word!("by"),
|
||||
word!("wide"),
|
||||
word!("road"),
|
||||
word!("at"),
|
||||
word!("dusk"),
|
||||
punct!("."),
|
||||
word!("sam", "Sam"),
|
||||
word!("can"),
|
||||
word!("see"),
|
||||
word!("rain"),
|
||||
word!("end"),
|
||||
word!("and"),
|
||||
word!("sun"),
|
||||
word!("shine"),
|
||||
word!("at"),
|
||||
word!("last"),
|
||||
punct!("."),
|
||||
word!("ben", "Ben"),
|
||||
word!("can"),
|
||||
word!("see"),
|
||||
word!("night"),
|
||||
word!("sky"),
|
||||
word!("and"),
|
||||
word!("bright"),
|
||||
word!("red"),
|
||||
word!("camp"),
|
||||
word!("lights"),
|
||||
punct!("."),
|
||||
word!("tim", "Tim"),
|
||||
word!("and"),
|
||||
word!("gus", "Gus"),
|
||||
word!("grin"),
|
||||
word!("and"),
|
||||
word!("wave"),
|
||||
word!("at"),
|
||||
word!("camp"),
|
||||
word!("at"),
|
||||
word!("last"),
|
||||
punct!("."),
|
||||
word!("sam", "Sam"),
|
||||
punct!(","),
|
||||
word!("ben", "Ben"),
|
||||
punct!(","),
|
||||
word!("tim", "Tim"),
|
||||
punct!(","),
|
||||
word!("and"),
|
||||
word!("gus", "Gus"),
|
||||
word!("plan"),
|
||||
word!("next"),
|
||||
word!("big"),
|
||||
word!("camp"),
|
||||
word!("trip"),
|
||||
punct!("."),
|
||||
];
|
||||
|
||||
pub const READING_TEXTS: &[ReadingText] = &[ReadingText {
|
||||
id: "basic-rules-01",
|
||||
title: "Текст для чтения",
|
||||
tokens: BASIC_RULES_TOKENS,
|
||||
}];
|
||||
|
||||
pub fn word_by_id(id: &str) -> Option<&'static Word> {
|
||||
DICTIONARY.iter().find(|word| word.id == id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn tokens_to_text(tokens: &[Token]) -> String {
|
||||
let mut output = String::new();
|
||||
let mut needs_space = false;
|
||||
for token in tokens {
|
||||
match token {
|
||||
Token::Word { surface, .. } => {
|
||||
if needs_space {
|
||||
output.push(' ');
|
||||
}
|
||||
output.push_str(surface);
|
||||
needs_space = true;
|
||||
}
|
||||
Token::Punctuation(value) => {
|
||||
output.push_str(value);
|
||||
needs_space = !matches!(*value, "…" | "—" | "(");
|
||||
}
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::Path;
|
||||
|
||||
fn source_text() -> &'static str {
|
||||
let source =
|
||||
include_str!("../../english/ex/theme-13-reading-rules/reading-text-01-basic-rules.md");
|
||||
source
|
||||
.split_once("<!-- text-start -->")
|
||||
.unwrap()
|
||||
.1
|
||||
.split_once("<!-- text-end -->")
|
||||
.unwrap()
|
||||
.0
|
||||
.trim()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tokens_render_exact_source_text() {
|
||||
let text = &READING_TEXTS[0];
|
||||
assert_eq!(tokens_to_text(text.tokens), source_text());
|
||||
assert_eq!(
|
||||
text.tokens
|
||||
.iter()
|
||||
.filter(|token| matches!(token, Token::Word { .. }))
|
||||
.count(),
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_words_reuse_stable_ids() {
|
||||
let sam_ids: Vec<_> = READING_TEXTS[0]
|
||||
.tokens
|
||||
.iter()
|
||||
.filter_map(|token| match token {
|
||||
Token::Word { word_id, surface } if *surface == "Sam" => Some(*word_id),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert!(sam_ids.len() > 1);
|
||||
assert!(sam_ids.iter().all(|id| *id == "sam"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dictionary_ids_are_unique_and_audio_files_exist() {
|
||||
assert_eq!(DICTIONARY.len(), 97);
|
||||
assert_eq!(word_by_id("camp").unwrap().ipa_us, "kæmp");
|
||||
assert!(word_by_id("missing").is_none());
|
||||
for (index, word) in DICTIONARY.iter().enumerate() {
|
||||
assert!(
|
||||
!DICTIONARY[..index]
|
||||
.iter()
|
||||
.any(|previous| previous.id == word.id),
|
||||
"duplicate dictionary id: {}",
|
||||
word.id
|
||||
);
|
||||
}
|
||||
for word in DICTIONARY {
|
||||
let asset = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("public")
|
||||
.join(word.audio_path.trim_start_matches('/'));
|
||||
assert!(asset.is_file(), "missing audio: {}", asset.display());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_text_token_references_exactly_one_dictionary_word() {
|
||||
for text in READING_TEXTS {
|
||||
for token in text.tokens {
|
||||
if let Token::Word { word_id, .. } = token {
|
||||
let matches = DICTIONARY.iter().filter(|word| word.id == *word_id).count();
|
||||
assert_eq!(matches, 1, "{} references {word_id}", text.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
mod data;
|
||||
|
||||
use data::{word_by_id, ReadingText, Token, Word, DICTIONARY, READING_TEXTS};
|
||||
use leptos::ev::KeyboardEvent;
|
||||
use leptos::prelude::*;
|
||||
use wasm_bindgen_futures::{spawn_local, JsFuture};
|
||||
use web_sys::HtmlInputElement;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum AudioState {
|
||||
Ready,
|
||||
Playing,
|
||||
PlaybackBlocked,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
mount_to_body(App);
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App() -> impl IntoView {
|
||||
let reading_text = &READING_TEXTS[0];
|
||||
let word_count = reading_text
|
||||
.tokens
|
||||
.iter()
|
||||
.filter(|token| matches!(token, Token::Word { .. }))
|
||||
.count();
|
||||
let selected_id = RwSignal::new(None::<&'static str>);
|
||||
let query = RwSignal::new(String::new());
|
||||
let audio_ref = NodeRef::<leptos::html::Audio>::new();
|
||||
let audio_state = RwSignal::new(AudioState::Ready);
|
||||
|
||||
let select_word = move |id: &'static str| {
|
||||
if let Some(audio) = audio_ref.get() {
|
||||
let _ = audio.pause();
|
||||
}
|
||||
audio_state.set(AudioState::Ready);
|
||||
selected_id.set(Some(id));
|
||||
};
|
||||
let activate_key = move |event: KeyboardEvent, id: &'static str| {
|
||||
if event.key() == "Enter" || event.key() == " " {
|
||||
event.prevent_default();
|
||||
select_word(id);
|
||||
}
|
||||
};
|
||||
|
||||
let toggle_audio = move |_| {
|
||||
if let Some(audio) = audio_ref.get() {
|
||||
if audio.paused() {
|
||||
match audio.play() {
|
||||
Ok(promise) => {
|
||||
let audio_state = audio_state;
|
||||
spawn_local(async move {
|
||||
if JsFuture::from(promise).await.is_err() {
|
||||
audio_state.set(AudioState::PlaybackBlocked);
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(_) => audio_state.set(AudioState::PlaybackBlocked),
|
||||
}
|
||||
} else {
|
||||
let _ = audio.pause();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
view! {
|
||||
<main class="lesson-shell">
|
||||
<header class="masthead">
|
||||
<p class="eyebrow">"Лаборатория чтения · Урок 01"</p>
|
||||
<h1>"Читаем" <em>"по правилам"</em></h1>
|
||||
<p class="lede">{format!("{word_count} слов для спокойной тренировки. Нажмите на слово, чтобы увидеть его чтение.")}</p>
|
||||
</header>
|
||||
|
||||
<section class="reading-card" aria-labelledby="reading-heading">
|
||||
<div class="section-heading">
|
||||
<p class="section-number">"01"</p>
|
||||
<div>
|
||||
<h2 id="reading-heading">{reading_text.title}</h2>
|
||||
<p>"Каждое слово — отдельная карточка для слуха и произношения."</p>
|
||||
</div>
|
||||
<p class="word-count">{format!("{word_count} слов")}</p>
|
||||
</div>
|
||||
<p class="reading-text">
|
||||
{render_reading_text(reading_text, selected_id, select_word, activate_key)}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="detail-panel" aria-live="polite" aria-labelledby="detail-heading">
|
||||
{move || selected_id.get().and_then(word_by_id).map(|word| detail_view(word, audio_ref, audio_state, toggle_audio)).unwrap_or_else(|| view! {
|
||||
<div class="detail-empty">
|
||||
<p class="detail-kicker">"Словарная карточка"</p>
|
||||
<h2 id="detail-heading">"Выберите слово в тексте"</h2>
|
||||
<p>"Карточка покажет написание, американское произношение и русскую подсказку."</p>
|
||||
</div>
|
||||
}.into_any())}
|
||||
</section>
|
||||
|
||||
<section class="dictionary-section" aria-labelledby="dictionary-heading">
|
||||
<div class="dictionary-heading">
|
||||
<p class="section-number">"02"</p>
|
||||
<div>
|
||||
<h2 id="dictionary-heading">"Внутренний словарь"</h2>
|
||||
<p>"Все слова урока; поиск по написанию, IPA или русской подсказке."</p>
|
||||
</div>
|
||||
</div>
|
||||
<label class="search-label" for="dictionary-search">"Найти слово"</label>
|
||||
<input
|
||||
id="dictionary-search"
|
||||
type="search"
|
||||
placeholder="например, camp или кэмп"
|
||||
on:input=move |event| query.set(event_target::<HtmlInputElement>(&event).value())
|
||||
/>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>"Слово"</th><th>"US IPA"</th><th>"Подсказка"</th><th>"Аудио"</th></tr></thead>
|
||||
<tbody>
|
||||
{move || {
|
||||
let needle = query.get().to_lowercase();
|
||||
DICTIONARY.iter().filter(|word| needle.is_empty() || word.spelling.contains(&needle) || word.ipa_us.contains(&needle) || word.russian_hint.to_lowercase().contains(&needle)).map(|word| {
|
||||
let id = word.id;
|
||||
view! {
|
||||
<tr>
|
||||
<td><button type="button" class="table-word" on:click=move |_| select_word(id)>{word.spelling}</button></td>
|
||||
<td>{word.ipa_us}</td>
|
||||
<td>{word.russian_hint}</td>
|
||||
<td>
|
||||
<audio controls=true preload="none" src=word.audio_path>
|
||||
"Ваш браузер не поддерживает аудио."
|
||||
</audio>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}).collect_view()
|
||||
}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
fn render_reading_text(
|
||||
reading_text: &'static ReadingText,
|
||||
selected_id: RwSignal<Option<&'static str>>,
|
||||
select_word: impl Fn(&'static str) + Copy + 'static,
|
||||
activate_key: impl Fn(KeyboardEvent, &'static str) + Copy + 'static,
|
||||
) -> AnyView {
|
||||
reading_text
|
||||
.tokens
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, token)| match token {
|
||||
Token::Word { word_id, surface } => {
|
||||
let id = *word_id;
|
||||
let leading_space = if index == 0 { "" } else { " " };
|
||||
view! {
|
||||
<span class="word-token">
|
||||
{leading_space}
|
||||
<button
|
||||
class="reading-word"
|
||||
class:active=move || selected_id.get() == Some(id)
|
||||
type="button"
|
||||
aria-label=format!("Открыть слово {surface}")
|
||||
on:click=move |_| select_word(id)
|
||||
on:keydown=move |event| activate_key(event, id)
|
||||
>{*surface}</button>
|
||||
</span>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
Token::Punctuation(value) => {
|
||||
view! { <span class="punctuation">{*value}</span> }.into_any()
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn detail_view(
|
||||
word: &'static Word,
|
||||
audio_ref: NodeRef<leptos::html::Audio>,
|
||||
audio_state: RwSignal<AudioState>,
|
||||
toggle_audio: impl Fn(leptos::ev::MouseEvent) + Copy + 'static,
|
||||
) -> AnyView {
|
||||
view! {
|
||||
<div class="detail-content">
|
||||
<p class="detail-kicker">"Словарная карточка"</p>
|
||||
<h2 id="detail-heading">{word.spelling}</h2>
|
||||
<dl class="pronunciation-grid">
|
||||
<div><dt>"Американский IPA"</dt><dd>{word.ipa_us}</dd></div>
|
||||
<div><dt>"Русская подсказка"</dt><dd>{word.russian_hint}</dd></div>
|
||||
</dl>
|
||||
<audio
|
||||
node_ref=audio_ref
|
||||
src=word.audio_path
|
||||
preload="none"
|
||||
on:play=move |_| audio_state.set(AudioState::Playing)
|
||||
on:pause=move |_| if audio_state.get() != AudioState::Unavailable { audio_state.set(AudioState::Ready) }
|
||||
on:ended=move |_| audio_state.set(AudioState::Ready)
|
||||
on:error=move |_| audio_state.set(AudioState::Unavailable)
|
||||
/>
|
||||
<button
|
||||
class="audio-button"
|
||||
type="button"
|
||||
disabled=move || audio_state.get() == AudioState::Unavailable
|
||||
aria-pressed=move || audio_state.get() == AudioState::Playing
|
||||
aria-label=move || audio_button_label(audio_state.get(), word.spelling)
|
||||
on:click=toggle_audio
|
||||
>
|
||||
{move || audio_button_text(audio_state.get())}
|
||||
</button>
|
||||
<p class="audio-note" aria-live="polite">{move || audio_status_text(audio_state.get())}</p>
|
||||
</div>
|
||||
}.into_any()
|
||||
}
|
||||
|
||||
fn audio_button_label(state: AudioState, spelling: &str) -> String {
|
||||
match state {
|
||||
AudioState::Ready => format!("Слушать: {spelling}"),
|
||||
AudioState::Playing => format!("Пауза: {spelling}"),
|
||||
AudioState::PlaybackBlocked => format!("Повторить воспроизведение: {spelling}"),
|
||||
AudioState::Unavailable => format!("Аудио недоступно: {spelling}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn audio_button_text(state: AudioState) -> &'static str {
|
||||
match state {
|
||||
AudioState::Ready => "Слушать",
|
||||
AudioState::Playing => "Пауза",
|
||||
AudioState::PlaybackBlocked => "Повторить",
|
||||
AudioState::Unavailable => "Аудио недоступно",
|
||||
}
|
||||
}
|
||||
|
||||
fn audio_status_text(state: AudioState) -> &'static str {
|
||||
match state {
|
||||
AudioState::Ready => "Запись: американский английский · MP3",
|
||||
AudioState::Playing => "Воспроизводится: американский английский · MP3",
|
||||
AudioState::PlaybackBlocked => "Браузер не разрешил начать запись. Нажмите «Повторить».",
|
||||
AudioState::Unavailable => {
|
||||
"Не удалось открыть локальный MP3. Проверьте файл и повторите попытку."
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user