use core::panic; use std::str::FromStr; use comrak::format_html; use serde::{Deserialize, Deserializer, Serialize}; use serde_yaml::Value; use crate::{prelude::*, utils::furigana_to_html}; #[derive(Serialize, Deserialize)] pub struct Challenge { pub text: Option, pub japanese: Option>>>, pub english: Option>, pub song: Option, pub youtube: Option, pub spotify: Option, pub translation: Option, pub suggester: Option, pub date: chrono::NaiveDate, } impl Challenge { pub fn get(number: u32) -> Self { use comrak::{parse_document, Arena, ComrakOptions}; use std::fs; let options = { let mut options = ComrakOptions::default(); options.extension.front_matter_delimiter = Some("---".to_owned()); options }; let arena = Arena::new(); let challenge_text = fs::read_to_string(format!("content/challenges/{number}.md")) .expect("Couldn't find challenge file"); let root = parse_document( &arena, &(challenge_text // comrak can't find frontmatter if there's only frontmatter and no newline at end // TODO: Open issue in comrak + "\n"), &options, ); if let Some(node) = root.children().next() { if let comrak::nodes::NodeValue::FrontMatter(frontmatter) = &node.data.borrow().value { let frontmatter = { // Trim starting and ending fences let lines: Vec<&str> = frontmatter.trim().lines().collect(); lines[1..lines.len() - 1].join("\n") }; let mut challenge: Challenge = serde_yaml::from_str(&frontmatter).unwrap(); //challenge.text = Some(challenge_text.replace(&frontmatter, "").trim().to_owned()); let mut html = vec![]; format_html(root, &ComrakOptions::default(), &mut html) .expect("Failed to format HTML"); challenge.text = Some(furigana_to_html(&gh_emoji::Replacer::new() .replace_all(&String::from_utf8(html).unwrap()))); challenge } else { panic!("No frontmatter!") } } else { panic!("Empty document!") } } } #[derive(Serialize, Deserialize)] pub struct Song { pub japanese: Option, pub english: Option, } #[derive(Serialize)] pub struct ChallengeWord { pub dictionary: Option, pub pos: Option, pub text: Option>, } impl<'de> Deserialize<'de> for ChallengeWord { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let value = Deserialize::deserialize(deserializer)?; let map = if let Value::Mapping(map) = value { map } else { return Err(serde::de::Error::invalid_type( serde::de::Unexpected::Other("not a string or map"), &"a string or map", )); }; let text = map.get("text").map(|value| match value { Value::String(string) => vec![Furigana::new(string.clone(), None)], Value::Sequence(sequence) => sequence .iter() .map(|value| match value { Value::String(kanji) => Furigana::new(kanji.to_owned(), None), Value::Mapping(mapping) => { let (kanji, furigana) = mapping.iter().next().unwrap(); Furigana::new( kanji.as_str().unwrap().to_owned(), Some(furigana.as_str().unwrap().to_owned()), ) } _ => panic!(), }) .collect(), _ => panic!(), }); let dictionary = match map.get("dictionary") { Some(value) => match value { Value::Null => None, Value::String(dictionary) => Some(if !dictionary.starts_with("http") { format!("https://jisho.org/word/{dictionary}") } else { dictionary.to_owned() }), _ => panic!("dictionary must be string!"), }, None => text.as_ref().map(|furigana| { furigana .iter() .map(|segment| segment.kanji.clone()) .collect() }), }; let pos: Option = map .get("pos") .map(|value| value.as_str().unwrap().parse().unwrap()); Ok(ChallengeWord { dictionary, pos, text, }) } } #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum PartOfSpeech { Noun, Adjective, Verb, Adverb, Particle, Phrase, } #[derive(Debug, PartialEq, Eq)] pub struct ParsePartOfSpeechError; impl FromStr for PartOfSpeech { type Err = ParsePartOfSpeechError; fn from_str(s: &str) -> Result { use PartOfSpeech::*; Ok(match s { "noun" => Noun, "adjective" => Adjective, "verb" => Verb, "adverb" => Adverb, "particle" => Particle, "phrase" => Phrase, _ => return Err(ParsePartOfSpeechError), }) } } #[derive(Serialize, Deserialize)] pub struct Furigana { pub kanji: String, pub kyujitai: Option, pub furigana: Option, } impl Furigana { fn new(kanji: String, furigana: Option) -> Self { Self { kyujitai: match kanji.to_kyujitai() { kyujitai if kyujitai.eq(&kanji) => None, kyujitai => Some(kyujitai), }, kanji, furigana, } } } #[derive(Serialize, Deserialize)] pub struct Translation { pub author: Option, pub site: TranslationSite, } #[derive(Serialize, Deserialize)] pub struct TranslationSite { pub name: String, pub link: String, }