cargo fmt
This commit is contained in:
parent
6286b73b4b
commit
79705d03a4
19 changed files with 178 additions and 115 deletions
|
@ -9,4 +9,4 @@ pub mod user {
|
|||
pub const USER_AVATAR_COOKIE: &str = "user_avatar";
|
||||
}
|
||||
pub const LANG_COOKIE: &str = "lang";
|
||||
pub const WELCOMED_COOKIE: &str = "welcomed";
|
||||
pub const WELCOMED_COOKIE: &str = "welcomed";
|
||||
|
|
37
src/main.rs
37
src/main.rs
|
@ -1,14 +1,17 @@
|
|||
#[macro_use]
|
||||
extern crate rocket;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use poise::serenity_prelude::Http;
|
||||
use rocket::{fs::{relative, FileServer}, Rocket, Ignite};
|
||||
use rocket::{
|
||||
fs::{relative, FileServer},
|
||||
Ignite, Rocket,
|
||||
};
|
||||
use rocket_dyn_templates::{tera, Template};
|
||||
use std::{collections::HashMap, env};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
mod models;
|
||||
use models::{Settings, Database};
|
||||
use models::{Database, Settings};
|
||||
|
||||
mod utils;
|
||||
|
||||
|
@ -52,13 +55,14 @@ async fn main() {
|
|||
}
|
||||
let http = http();
|
||||
load_database()
|
||||
.load_legacy(&http).await
|
||||
.load_legacy(&http)
|
||||
.await
|
||||
.expect("Failed to load legacy submissions");
|
||||
},
|
||||
}
|
||||
},
|
||||
None => {
|
||||
rocket().await.expect("Failed to launch rocket");
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,7 +90,16 @@ async fn rocket() -> Result<Rocket<Ignite>, rocket::Error> {
|
|||
.manage(load_database())
|
||||
.mount(
|
||||
"/",
|
||||
routes![get_challenge, get_guilds, get_user, login, post_login, success, logout, testing],
|
||||
routes![
|
||||
get_challenge,
|
||||
get_guilds,
|
||||
get_user,
|
||||
login,
|
||||
post_login,
|
||||
success,
|
||||
logout,
|
||||
testing
|
||||
],
|
||||
)
|
||||
.mount("/css", FileServer::from(relative!("styles/css")))
|
||||
.mount("/", FileServer::from(relative!("assets")).rank(2))
|
||||
|
@ -108,12 +121,12 @@ async fn rocket() -> Result<Rocket<Ignite>, rocket::Error> {
|
|||
);
|
||||
engines.tera.register_filter(
|
||||
"furigana",
|
||||
move |value: &Value, args: &HashMap<String, Value>| {
|
||||
furigana_filter(value, args)
|
||||
},
|
||||
move |value: &Value, args: &HashMap<String, Value>| furigana_filter(value, args),
|
||||
)
|
||||
}))
|
||||
.ignite().await
|
||||
.ignite()
|
||||
.await
|
||||
.unwrap()
|
||||
.launch().await
|
||||
.launch()
|
||||
.await
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ impl Challenge {
|
|||
.expect("Couldn't find challenge file");
|
||||
let root = parse_document(
|
||||
&arena,
|
||||
&(challenge_text
|
||||
&(challenge_text
|
||||
// comrak can't find frontmatter if there's only frontmatter and no newline at end
|
||||
// TODO: Open issue in comrak
|
||||
+ "\n"),
|
||||
|
@ -53,8 +53,9 @@ impl Challenge {
|
|||
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.text = Some(furigana_to_html(
|
||||
&gh_emoji::Replacer::new().replace_all(&String::from_utf8(html).unwrap()),
|
||||
));
|
||||
challenge
|
||||
} else {
|
||||
panic!("No frontmatter!")
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
use std::collections::HashSet;
|
||||
use std::{fs::File, io::Read, collections::HashMap, path::Path};
|
||||
use std::{collections::HashMap, fs::File, io::Read, path::Path};
|
||||
|
||||
use poise::serenity_prelude::{SerenityError, Http};
|
||||
use r2d2::{Pool, PooledConnection};
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
use r2d2_sqlite::rusqlite::{self, params};
|
||||
use derive_more::From;
|
||||
use poise::serenity_prelude::{Http, SerenityError};
|
||||
use r2d2::{Pool, PooledConnection};
|
||||
use r2d2_sqlite::rusqlite::{self, params};
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
|
||||
use crate::{utils::get_challenge_number, models::User};
|
||||
use crate::{models::User, utils::get_challenge_number};
|
||||
|
||||
use super::{LegacySubmission, Submission};
|
||||
|
||||
|
@ -38,9 +38,7 @@ impl Database {
|
|||
Path::new(DATABASE_FILENAME).exists()
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
testing: bool,
|
||||
) -> Result<Self> {
|
||||
pub fn new(testing: bool) -> Result<Self> {
|
||||
let connection_manager = if testing {
|
||||
SqliteConnectionManager::memory()
|
||||
} else {
|
||||
|
@ -77,7 +75,8 @@ impl Database {
|
|||
}
|
||||
|
||||
pub fn has_submitted(&self, user_id: u64) -> Result<bool> {
|
||||
Ok(self.conn()?
|
||||
Ok(self
|
||||
.conn()?
|
||||
.prepare("SELECT 1 FROM User WHERE id = ?1 LIMIT 1")?
|
||||
.query_row(params![user_id], |_row| Ok(()))
|
||||
.is_ok())
|
||||
|
@ -103,38 +102,47 @@ impl Database {
|
|||
// their username/discriminator since their previous submission
|
||||
match archived_users.get(&id) {
|
||||
Some(User { deleted, .. }) => {
|
||||
archived_users.insert(id, User {
|
||||
archived_users.insert(
|
||||
id,
|
||||
avatar: None,
|
||||
deleted: *deleted,
|
||||
..User::from_username(&legacy.username)
|
||||
});
|
||||
},
|
||||
None => match User::fetch(http, id).await {
|
||||
Ok(User { deleted: true, .. }) => {
|
||||
archived_users.insert(id, User {
|
||||
User {
|
||||
id,
|
||||
avatar: None,
|
||||
deleted: true,
|
||||
deleted: *deleted,
|
||||
..User::from_username(&legacy.username)
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
None => match User::fetch(http, id).await {
|
||||
Ok(User { deleted: true, .. }) => {
|
||||
archived_users.insert(
|
||||
id,
|
||||
User {
|
||||
id,
|
||||
avatar: None,
|
||||
deleted: true,
|
||||
..User::from_username(&legacy.username)
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(user) => {
|
||||
conn.execute(
|
||||
"INSERT INTO User(id, name, discriminator, avatar, deleted) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
params![user.id, user.name, user.discriminator, user.avatar, user.deleted]
|
||||
).map_err(DatabaseError::Rusqlite)?;
|
||||
},
|
||||
}
|
||||
Err(error) if error.to_string().eq("Unknown User") => {
|
||||
// This will also be called in the case of an invalid user ID
|
||||
println!("Failed to fetch user {id}, adding to archive");
|
||||
archived_users.insert(id, User {
|
||||
archived_users.insert(
|
||||
id,
|
||||
avatar: None,
|
||||
deleted: false,
|
||||
..User::from_username(&legacy.username)
|
||||
});
|
||||
},
|
||||
User {
|
||||
id,
|
||||
avatar: None,
|
||||
deleted: false,
|
||||
..User::from_username(&legacy.username)
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(error) => return Err(LoadLegacyError::Serenity(error)),
|
||||
},
|
||||
};
|
||||
|
@ -160,8 +168,11 @@ impl Database {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_challenge_user_data(&self, challenge: u32) -> Result<(Vec<Submission>, HashMap<String, User>)> {
|
||||
|
||||
pub fn get_challenge_user_data(
|
||||
&self,
|
||||
challenge: u32,
|
||||
) -> Result<(Vec<Submission>, HashMap<String, User>)> {
|
||||
let submissions = self.get_submissions(challenge)?;
|
||||
let users = self.get_users({
|
||||
let mut user_ids = HashSet::new();
|
||||
|
@ -174,7 +185,8 @@ impl Database {
|
|||
}
|
||||
|
||||
pub fn get_submissions(&self, challenge: u32) -> Result<Vec<Submission>> {
|
||||
Ok(self.conn()?
|
||||
Ok(self
|
||||
.conn()?
|
||||
.prepare("SELECT author_id, timestamp, image FROM Submission WHERE challenge = ?1")?
|
||||
.query_map(params![challenge], |row| {
|
||||
Ok(Submission {
|
||||
|
@ -188,7 +200,8 @@ impl Database {
|
|||
}
|
||||
|
||||
pub fn get_user_by_name(&self, name: &str) -> Result<Option<User>> {
|
||||
Ok(self.conn()?
|
||||
Ok(self
|
||||
.conn()?
|
||||
.prepare("SELECT id, discriminator, avatar, deleted FROM User where name = ?1")?
|
||||
.query_row(params![name], |row| {
|
||||
Ok(Some(User {
|
||||
|
@ -202,8 +215,10 @@ impl Database {
|
|||
}
|
||||
|
||||
pub fn get_submissions_by_user_name(&self, name: &str) -> Result<Vec<Submission>> {
|
||||
Ok(self.conn()?
|
||||
.prepare("
|
||||
Ok(self
|
||||
.conn()?
|
||||
.prepare(
|
||||
"
|
||||
SELECT author_id, timestamp, image, challenge
|
||||
FROM Submission s
|
||||
JOIN User u ON s.author_id = u.id
|
||||
|
@ -227,16 +242,23 @@ impl Database {
|
|||
.iter()
|
||||
// u64 must be converted to String for templates
|
||||
.map(|id| -> Result<(String, User)> {
|
||||
match conn.prepare("SELECT name, discriminator, avatar, deleted FROM User WHERE id = ?1") {
|
||||
Ok(mut statement) => statement.query_row(params![id], |row| {
|
||||
Ok((id.to_string(), User {
|
||||
id: *id,
|
||||
name: row.get(0)?,
|
||||
discriminator: row.get(1)?,
|
||||
avatar: row.get(2)?,
|
||||
deleted: row.get(3)?,
|
||||
}))
|
||||
}).map_err(DatabaseError::Rusqlite),
|
||||
match conn
|
||||
.prepare("SELECT name, discriminator, avatar, deleted FROM User WHERE id = ?1")
|
||||
{
|
||||
Ok(mut statement) => statement
|
||||
.query_row(params![id], |row| {
|
||||
Ok((
|
||||
id.to_string(),
|
||||
User {
|
||||
id: *id,
|
||||
name: row.get(0)?,
|
||||
discriminator: row.get(1)?,
|
||||
avatar: row.get(2)?,
|
||||
deleted: row.get(3)?,
|
||||
},
|
||||
))
|
||||
})
|
||||
.map_err(DatabaseError::Rusqlite),
|
||||
Err(error) => Err(DatabaseError::Rusqlite(error)),
|
||||
}
|
||||
})
|
||||
|
@ -255,4 +277,4 @@ impl Database {
|
|||
// For new submissions only
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
mod user;
|
||||
pub use user::{Username, User, SessionUser};
|
||||
pub use user::{SessionUser, User, Username};
|
||||
|
||||
mod challenge;
|
||||
pub use challenge::Challenge;
|
||||
|
@ -11,4 +11,4 @@ mod submission;
|
|||
pub use submission::*;
|
||||
|
||||
mod database;
|
||||
pub use database::{Database, DatabaseError};
|
||||
pub use database::{Database, DatabaseError};
|
||||
|
|
|
@ -51,11 +51,13 @@ pub struct Guild {
|
|||
impl Guild {
|
||||
pub async fn load(&mut self, http: &Http) -> poise::serenity_prelude::Result<()> {
|
||||
let server = http.get_guild(self.id).await?;
|
||||
self.icon = Some(server.icon_url().map(|icon| if icon.contains("/a_") {
|
||||
// serenity only gives non-animated URL
|
||||
icon.replace("webp", "gif")
|
||||
} else {
|
||||
icon
|
||||
self.icon = Some(server.icon_url().map(|icon| {
|
||||
if icon.contains("/a_") {
|
||||
// serenity only gives non-animated URL
|
||||
icon.replace("webp", "gif")
|
||||
} else {
|
||||
icon
|
||||
}
|
||||
}));
|
||||
self.name = Some(server.name);
|
||||
Ok(())
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use chrono::{Utc, TimeZone};
|
||||
use serde::Deserialize;
|
||||
use chrono::{TimeZone, Utc};
|
||||
use derive_more::From;
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::Submission;
|
||||
|
||||
|
@ -28,7 +28,8 @@ pub enum LegacySubmissionParseError {
|
|||
impl LegacySubmission {
|
||||
pub fn parse(&self, challenge: u32) -> Result<Vec<Submission>, LegacySubmissionParseError> {
|
||||
let author_id = self.id;
|
||||
Ok(self.images
|
||||
Ok(self
|
||||
.images
|
||||
.iter()
|
||||
.map(|image| {
|
||||
Submission {
|
||||
|
@ -37,20 +38,25 @@ impl LegacySubmission {
|
|||
// The last number of the filename is either a discriminator or a datestamp
|
||||
// We can split apart the filename and check if it's >9999. In that case,
|
||||
// it's a datestamp.
|
||||
(|| image
|
||||
// Get filename without extension
|
||||
.split('.').next()?
|
||||
// Get last number
|
||||
.split('-')
|
||||
.last()?
|
||||
.parse()
|
||||
.ok()
|
||||
// Check if discriminator or timestamp, then convert
|
||||
.map(|number| if number > 9999 {
|
||||
Utc.timestamp_millis_opt(number).single()
|
||||
} else {
|
||||
None
|
||||
})?)()
|
||||
(|| {
|
||||
image
|
||||
// Get filename without extension
|
||||
.split('.')
|
||||
.next()?
|
||||
// Get last number
|
||||
.split('-')
|
||||
.last()?
|
||||
.parse()
|
||||
.ok()
|
||||
// Check if discriminator or timestamp, then convert
|
||||
.map(|number| {
|
||||
if number > 9999 {
|
||||
Utc.timestamp_millis_opt(number).single()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?
|
||||
})()
|
||||
},
|
||||
image: image.clone(),
|
||||
challenge,
|
||||
|
@ -58,4 +64,4 @@ impl LegacySubmission {
|
|||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ mod submission;
|
|||
pub use submission::Submission;
|
||||
|
||||
mod legacy_submission;
|
||||
pub use legacy_submission::LegacySubmission;
|
||||
pub use legacy_submission::LegacySubmission;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use chrono::{Utc, DateTime};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::Serialize;
|
||||
|
||||
// Challenge submission
|
||||
|
@ -9,7 +9,7 @@ use serde::Serialize;
|
|||
pub struct Submission {
|
||||
pub author_id: u64,
|
||||
// Some fields might be empty for legacy submissions
|
||||
// Starting during challenge #87, submissions have a datestamp appended to their filename.
|
||||
// Starting during challenge #87, submissions have a datestamp appended to their filename.
|
||||
// TODO: Determine whether this datestamp is local time or UTC
|
||||
pub timestamp: Option<DateTime<Utc>>,
|
||||
// Image path relative to submission folder.
|
||||
|
@ -18,4 +18,4 @@ pub struct Submission {
|
|||
// Not necessary for challenge pages,
|
||||
// but needed for user profile pages
|
||||
pub challenge: u32,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ mod tests;
|
|||
|
||||
use chrono::Utc;
|
||||
use derive_more::From;
|
||||
use poise::serenity_prelude::{self, UserId, Http};
|
||||
use poise::serenity_prelude::{self, Http, UserId};
|
||||
use regex::Regex;
|
||||
use reqwest::StatusCode;
|
||||
use rocket::http::{Cookie, CookieJar};
|
||||
|
@ -46,7 +46,9 @@ impl Username for User {
|
|||
}
|
||||
|
||||
fn is_name_deleted(name: &str) -> bool {
|
||||
Regex::new(r"Deleted User [a-f0-9]{8}").unwrap().is_match(name)
|
||||
Regex::new(r"Deleted User [a-f0-9]{8}")
|
||||
.unwrap()
|
||||
.is_match(name)
|
||||
}
|
||||
|
||||
impl User {
|
||||
|
@ -54,10 +56,7 @@ impl User {
|
|||
let (name, discriminator) = {
|
||||
let mut iter = username.split('#');
|
||||
let name = iter.next().unwrap().to_owned();
|
||||
let discriminator = iter
|
||||
.next()
|
||||
.map(|str| str.parse().unwrap())
|
||||
.unwrap_or(0);
|
||||
let discriminator = iter.next().map(|str| str.parse().unwrap()).unwrap_or(0);
|
||||
(name, discriminator)
|
||||
};
|
||||
Self {
|
||||
|
@ -86,12 +85,19 @@ impl User {
|
|||
"https://cdn.discordapp.com/avatars/{}/{}.{}?size=1024",
|
||||
self.id,
|
||||
avatar,
|
||||
if avatar.starts_with("a_") { "gif" } else { "webp" }
|
||||
if avatar.starts_with("a_") {
|
||||
"gif"
|
||||
} else {
|
||||
"webp"
|
||||
}
|
||||
),
|
||||
// Archived user or user with no avatar, calculate default avatar
|
||||
// https://www.reddit.com/r/discordapp/comments/au6v4e/comment/eh61dm6/
|
||||
// https://docs.rs/serenity/0.11.5/serenity/model/user/struct.User.html#method.default_avatar_url
|
||||
None => format!("https://cdn.discordapp.com/embed/avatars/{}.png", self.discriminator % 5),
|
||||
None => format!(
|
||||
"https://cdn.discordapp.com/embed/avatars/{}.png",
|
||||
self.discriminator % 5
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,4 +27,4 @@ fn is_name_deleted() {
|
|||
assert!(!is_name_deleted("Deleted User Ce34a7da")); // capital letter in hex
|
||||
assert!(!is_name_deleted("Deleted User ce34a7d")); // hex too short
|
||||
assert!(!is_name_deleted("Deleted User")); // no hex
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ use rocket_dyn_templates::{context, Template};
|
|||
use crate::{
|
||||
cookies::LANG_COOKIE,
|
||||
i18n::DEFAULT as DEFAULT_LANG,
|
||||
models::{Challenge, Settings, SessionUser, Database},
|
||||
models::{Challenge, Database, SessionUser, Settings},
|
||||
utils::AcceptLanguage,
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use poise::serenity_prelude::Http;
|
||||
use rocket::{http::CookieJar, State};
|
||||
use rocket::response::stream::{Event, EventStream};
|
||||
use rocket::{http::CookieJar, State};
|
||||
|
||||
use crate::{cookies::user::USER_ID_COOKIE, models::Settings};
|
||||
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
use std::ops::Deref;
|
||||
|
||||
|
||||
use rocket::{http::{CookieJar, Status}, State};
|
||||
use rocket::{
|
||||
http::{CookieJar, Status},
|
||||
State,
|
||||
};
|
||||
use rocket_dyn_templates::{context, Template};
|
||||
|
||||
use crate::{
|
||||
cookies::LANG_COOKIE,
|
||||
i18n::DEFAULT as DEFAULT_LANG,
|
||||
models::{Settings, SessionUser, Database, DatabaseError},
|
||||
models::{Database, DatabaseError, SessionUser, Settings},
|
||||
utils::AcceptLanguage,
|
||||
};
|
||||
|
||||
|
@ -21,11 +23,13 @@ pub async fn get_user(
|
|||
) -> Result<Template, Status> {
|
||||
let profile_user = match database.get_user_by_name(&user) {
|
||||
Ok(profile_user) => profile_user,
|
||||
Err(DatabaseError::Rusqlite(rusqlite::Error::QueryReturnedNoRows)) => return Err(Status::NotFound),
|
||||
Err(DatabaseError::Rusqlite(rusqlite::Error::QueryReturnedNoRows)) => {
|
||||
return Err(Status::NotFound)
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("{:?}", error);
|
||||
return Err(Status::InternalServerError);
|
||||
},
|
||||
}
|
||||
};
|
||||
Ok(Template::render(
|
||||
"user",
|
||||
|
@ -41,4 +45,4 @@ pub async fn get_user(
|
|||
user: SessionUser::get(cookies).await.unwrap(),
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,10 @@ use rocket::{
|
|||
response::Redirect,
|
||||
};
|
||||
|
||||
use crate::{cookies::{token::TOKEN_EXPIRE_COOKIE, WELCOMED_COOKIE}, models::SessionUser};
|
||||
use crate::{
|
||||
cookies::{token::TOKEN_EXPIRE_COOKIE, WELCOMED_COOKIE},
|
||||
models::SessionUser,
|
||||
};
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct Login<'r> {
|
||||
|
|
|
@ -19,7 +19,8 @@ pub async fn testing(cookies: &CookieJar<'_>, http: &State<Http>) -> String {
|
|||
.await
|
||||
.expect("Failed to get logged in user data")
|
||||
.expect("No logged in user")
|
||||
.0.id
|
||||
.0
|
||||
.id
|
||||
)
|
||||
.await
|
||||
.expect("Failed to fetch user in server")
|
||||
|
|
|
@ -18,4 +18,4 @@ pub fn get_challenge_number() -> i32 {
|
|||
}
|
||||
}
|
||||
max
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,14 +12,19 @@ pub fn furigana_to_html(text: &str) -> String {
|
|||
// Curly brace literals \{ need to have their backslash escaped as \\{
|
||||
// TODO: Modify so <span lang="ja"> only wraps continuous sections of furigana
|
||||
let re = Regex::new(r"\[([^\]]*)\]\{([^\\}]*)}").unwrap();
|
||||
format!("<span lang=\"ja\">{}</span>", re.replace_all(text, "<ruby>$1<rp>(</rp><rt>$2</rt><rp>)</rp></ruby>"))
|
||||
format!(
|
||||
"<span lang=\"ja\">{}</span>",
|
||||
re.replace_all(text, "<ruby>$1<rp>(</rp><rt>$2</rt><rp>)</rp></ruby>")
|
||||
)
|
||||
}
|
||||
|
||||
pub fn furigana_filter(value: &Value, _args: &HashMap<String, Value>) -> tera::Result<Value> {
|
||||
if value.is_null() {
|
||||
return Ok(Value::String("".to_string()));
|
||||
}
|
||||
Ok(Value::String(furigana_to_html(value.as_str().expect("The furigana input must be a string"))))
|
||||
Ok(Value::String(furigana_to_html(
|
||||
value.as_str().expect("The furigana input must be a string"),
|
||||
)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -36,4 +41,4 @@ mod tests {
|
|||
</span>"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,4 +8,4 @@ mod challenge;
|
|||
pub use challenge::*;
|
||||
|
||||
mod furigana;
|
||||
pub use furigana::*;
|
||||
pub use furigana::*;
|
||||
|
|
Loading…
Add table
Reference in a new issue