Want to contribute? Fork me on Codeberg.org!
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

201 lines
6.2 KiB

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<String>,
pub japanese: Option<Vec<Vec<Vec<ChallengeWord>>>>,
pub english: Option<Vec<String>>,
pub song: Option<Song>,
pub youtube: Option<String>,
pub spotify: Option<String>,
pub translation: Option<Translation>,
pub suggester: Option<String>,
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<String>,
pub english: Option<String>,
}
#[derive(Serialize)]
pub struct ChallengeWord {
pub dictionary: Option<String>,
pub pos: Option<PartOfSpeech>,
pub text: Option<Vec<Furigana>>,
}
impl<'de> Deserialize<'de> for ChallengeWord {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<PartOfSpeech> = 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<Self, Self::Err> {
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<String>,
pub furigana: Option<String>,
}
impl Furigana {
fn new(kanji: String, furigana: Option<String>) -> 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<String>,
pub site: TranslationSite,
}
#[derive(Serialize, Deserialize)]
pub struct TranslationSite {
pub name: String,
pub link: String,
}