|
|
|
use core::panic;
|
|
|
|
use std::str::FromStr;
|
|
|
|
|
|
|
|
use serde::{Deserialize, Deserializer, Serialize};
|
|
|
|
use serde_yaml::Value;
|
|
|
|
|
|
|
|
use crate::{prelude::*};
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
|
|
pub struct Challenge {
|
|
|
|
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 root = parse_document(
|
|
|
|
&arena,
|
|
|
|
& (fs::read_to_string(format!("content/challenges/{number}.md"))
|
|
|
|
.expect("Couldn't find challenge file")
|
|
|
|
// 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 challenge: Challenge = serde_yaml::from_str(&frontmatter).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,
|
|
|
|
}
|