refactor page rendering to use an intermediate object

This commit is contained in:
kageru 2023-02-13 11:13:07 +01:00
parent c89f48e769
commit 556bbb3a08
4 changed files with 58 additions and 55 deletions

@ -11,6 +11,8 @@ mod data;
mod filter; mod filter;
mod parser; mod parser;
type AnyResult<T> = Result<T, Box<dyn std::error::Error>>;
// The yearly tins have ~250 cards in them. // The yearly tins have ~250 cards in them.
// I want to be higher than that so the page is usable as a set list. // I want to be higher than that so the page is usable as a set list.
const RESULT_LIMIT: usize = 300; const RESULT_LIMIT: usize = 300;
@ -55,6 +57,13 @@ struct Query {
q: String, q: String,
} }
#[derive(Debug)]
struct PageData {
title: String,
query: Option<String>,
body: String,
}
const HEADER: &str = include_str!("../static/header.html"); const HEADER: &str = include_str!("../static/header.html");
const HELP_CONTENT: &str = include_str!("../static/help.html"); const HELP_CONTENT: &str = include_str!("../static/help.html");
const FOOTER: &str = r#"<div id="bottom"> const FOOTER: &str = r#"<div id="bottom">
@ -65,7 +74,7 @@ const FOOTER: &str = r#"<div id="bottom">
</body></html>"#; </body></html>"#;
#[get("/")] #[get("/")]
async fn search(q: Option<Either<web::Query<Query>, web::Form<Query>>>) -> Result<HttpResponse, Box<dyn std::error::Error>> { async fn search(q: Option<Either<web::Query<Query>, web::Form<Query>>>) -> AnyResult<HttpResponse> {
let q = match q { let q = match q {
Some(Either::Left(web::Query(Query { q }))) => Some(q), Some(Either::Left(web::Query(Query { q }))) => Some(q),
Some(Either::Right(web::Form(Query { q }))) => Some(q), Some(Either::Right(web::Form(Query { q }))) => Some(q),
@ -73,54 +82,43 @@ async fn search(q: Option<Either<web::Query<Query>, web::Form<Query>>>) -> Resul
} }
.filter(|s| !s.is_empty()); .filter(|s| !s.is_empty());
let mut res = String::with_capacity(10_000); let mut res = String::with_capacity(10_000);
res.push_str(HEADER); let data = match q {
render_searchbox(&mut res, &q)?; Some(q) => compute_results(q)?,
match q { None => PageData { title: "YGO card search".to_owned(), query: None, body: "Enter a query above to search".to_owned() },
Some(q) => render_results(&mut res, &q)?, };
None => res.push_str("Enter a query above to search"), add_data(&mut res, &data)?;
}
finish_document(&mut res);
Ok(HttpResponse::Ok().insert_header(header::ContentType::html()).body(res)) Ok(HttpResponse::Ok().insert_header(header::ContentType::html()).body(res))
} }
#[get("/card/{id}")] #[get("/card/{id}")]
async fn card_info(card_id: web::Path<usize>) -> Result<HttpResponse, Box<dyn std::error::Error>> { async fn card_info(card_id: web::Path<usize>) -> AnyResult<HttpResponse> {
let mut res = String::with_capacity(2_000); let mut res = String::with_capacity(2_000);
res.push_str(HEADER); let data = match CARDS_BY_ID.get(&card_id) {
render_searchbox(&mut res, &None)?; Some(card) => PageData {
match CARDS_BY_ID.get(&card_id) { title: format!("{} - YGO Card Database", card.name),
Some(card) => { query: None,
res.push_str(r#""#); body: format!(
write!( r#"<div><img class="fullimage" src="{}/static/full/{}.jpg"/>{card}{}</div>"#,
res,
r#"
<div>
<img class="fullimage" src="{}/static/full/{}.jpg"/>
{card}
{}
</div>"#,
IMG_HOST.as_str(), IMG_HOST.as_str(),
card.id, card.id,
card.extended_info().unwrap_or_else(|_| String::new()), card.extended_info().unwrap_or_else(|_| String::new()),
)?; ),
} },
None => res.push_str("Card not found"), None => PageData { title: "Card not found - YGO Card Database".to_owned(), query: None, body: "Card not found".to_owned() },
} };
finish_document(&mut res); add_data(&mut res, &data)?;
Ok(HttpResponse::Ok().insert_header(header::ContentType::html()).body(res)) Ok(HttpResponse::Ok().insert_header(header::ContentType::html()).body(res))
} }
#[get("/help")] #[get("/help")]
async fn help() -> Result<HttpResponse, Box<dyn std::error::Error>> { async fn help() -> AnyResult<HttpResponse> {
let mut res = String::with_capacity(HEADER.len() + HELP_CONTENT.len() + FOOTER.len() + 250); let mut res = String::with_capacity(HEADER.len() + HELP_CONTENT.len() + FOOTER.len() + 250);
res.push_str(HEADER); let data = PageData { query: None, title: "Query Syntax - YGO Card Database".to_owned(), body: HELP_CONTENT.to_owned() };
render_searchbox(&mut res, &None)?; add_data(&mut res, &data)?;
res.push_str(HELP_CONTENT);
res.push_str(FOOTER);
Ok(HttpResponse::Ok().insert_header(header::ContentType::html()).body(res)) Ok(HttpResponse::Ok().insert_header(header::ContentType::html()).body(res))
} }
fn render_searchbox(res: &mut String, query: &Option<String>) -> std::fmt::Result { fn add_searchbox(res: &mut String, query: &Option<String>) -> std::fmt::Result {
write!( write!(
res, res,
r#" r#"
@ -134,12 +132,13 @@ fn render_searchbox(res: &mut String, query: &Option<String>) -> std::fmt::Resul
) )
} }
fn render_results(res: &mut String, query: &str) -> Result<(), Box<dyn std::error::Error>> { fn compute_results(raw_query: String) -> AnyResult<PageData> {
let (raw_filters, query) = match parser::parse_filters(query) { let mut body = String::with_capacity(10_000);
let (raw_filters, query) = match parser::parse_filters(&raw_query) {
Ok(q) => q, Ok(q) => q,
Err(e) => { Err(e) => {
write!(res, "Could not parse query: {e:?}")?; let s = format!("Could not parse query: {e:?}");
return Ok(()); return Ok(PageData { title: s.clone(), query: Some(raw_query), body: s });
} }
}; };
let now = Instant::now(); let now = Instant::now();
@ -149,30 +148,29 @@ fn render_results(res: &mut String, query: &str) -> Result<(), Box<dyn std::erro
.map(|c| CARDS_BY_ID.get(&c.id).unwrap()) .map(|c| CARDS_BY_ID.get(&c.id).unwrap())
.take(RESULT_LIMIT) .take(RESULT_LIMIT)
.collect(); .collect();
write!( let readable_query = format!("Showing {} results where {}", matches.len(), raw_filters.iter().map(|f| f.to_string()).join(" and "),);
res, write!(body, "<span class=\"meta\">{readable_query} (took {:?})</span>", now.elapsed())?;
"<span class=\"meta\">Showing {} results where {} (took {:?})</span>",
matches.len(),
raw_filters.iter().map(|f| f.to_string()).join(" and "),
now.elapsed()
)?;
if matches.is_empty() { if matches.is_empty() {
return Ok(()); return Ok(PageData { title: readable_query.clone(), query: Some(raw_query), body });
} }
res.push_str("<div style=\"display: flex; flex-wrap: wrap;\">"); body.push_str("<div style=\"display: flex; flex-wrap: wrap;\">");
for card in matches { for card in matches {
write!( write!(
res, body,
r#"<a class="cardresult" href="/card/{}"><img src="{}/static/thumb/{}.jpg" class="thumb"/>{card}</a>"#, r#"<a class="cardresult" href="/card/{}"><img src="{}/static/thumb/{}.jpg" class="thumb"/>{card}</a>"#,
card.id, card.id,
IMG_HOST.as_str(), IMG_HOST.as_str(),
card.id card.id
)?; )?;
} }
res.push_str("</div>"); body.push_str("</div>");
Ok(()) Ok(PageData { title: readable_query.clone(), query: Some(raw_query), body })
} }
fn finish_document(res: &mut String) { fn add_data(res: &mut String, pd: &PageData) -> AnyResult<()> {
res.push_str(FOOTER) res.push_str(&HEADER.replacen("{TITLE}", &pd.title, 1).replacen("{IMG_HOST}", &IMG_HOST, 1));
add_searchbox(res, &pd.query)?;
res.push_str(&pd.body);
res.push_str(FOOTER);
Ok(())
} }

@ -1,6 +1,8 @@
<html> <html>
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/style.css" /> <meta charset="UTF-8" />
<link rel="stylesheet" href="{IMG_HOST}/static/style.css" />
<title>{TITLE}</title>
</head> </head>
<body> <body>

@ -1,6 +1,7 @@
<h1>Query Syntax</h1> <h1>Query Syntax</h1>
The syntax is heavily inspired by <a href="https://scryfall.com/docs/syntax">Scryfall</a> with some changes and a lot fewer features.<br/> The syntax is heavily inspired by <a href="https://scryfall.com/docs/syntax">Scryfall</a> with some changes and a lot fewer features.<br/>
You can filter different characteristics of a card and combine multiple filters into one search. See below for examples. You can filter different characteristics of a card and combine multiple filters into one search. See below for examples.<br/>
<br/>
<h2>Search fields</h2> <h2>Search fields</h2>
Currently supported search fields are: Currently supported search fields are:
@ -12,13 +13,15 @@ Currently supported search fields are:
<li>The <code>type</code> (or <code>t</code>) of a card (this is “Warrior”, “Pyro”, “Insect”, etc. for monsters, but also “quick-play”, “counter”, or “normal” for Spells/Traps).</li> <li>The <code>type</code> (or <code>t</code>) of a card (this is “Warrior”, “Pyro”, “Insect”, etc. for monsters, but also “quick-play”, “counter”, or “normal” for Spells/Traps).</li>
<li>The <code>attribute</code> (or <code>attr</code> or <code>a</code>) of a card. This is “Light”, “Dark”, “Earth”, etc.</li> <li>The <code>attribute</code> (or <code>attr</code> or <code>a</code>) of a card. This is “Light”, “Dark”, “Earth”, etc.</li>
<li>The <code>text</code> (or <code>effect</code>, <code>eff</code>, <code>e</code>, or <code>o</code>) of a card. This is either the effect or flavor text (for normal monsters). For pendulum cards, this searches in both pendulum and monster effects. The <code>o</code> alias is to help my muscle memory coming from Scryfall.</li> <li>The <code>text</code> (or <code>effect</code>, <code>eff</code>, <code>e</code>, or <code>o</code>) of a card. This is either the effect or flavor text (for normal monsters). For pendulum cards, this searches in both pendulum and monster effects. The <code>o</code> alias is to help my muscle memory coming from Scryfall.</li>
<li>The <code>set</code> (or <code>s</code>) a card was printed in. This considers all printings, not just the original.</li> <li>The <code>set</code> (or <code>s</code>) a card was printed in. This considers all printings, not just the original, and uses the set code (e.g. <code>ioc</code> for Invasion of Chaos or <code>pote</code> for Power of the Elements).</li>
<li>The <code>copies</code> (or <code>legal</code>) you’re allowed to play according to the current banlist.</li> <li>The <code>copies</code> (or <code>legal</code>) you’re allowed to play according to the current banlist.</li>
</ul> </ul>
Anything not associated with a search field is interpreted as a search in the card name, so <a href="/?q=l%3A4+utopia"><code>l:4 utopia</code></a> will show all level/rank 4 monsters with “Utopia” in their name.<br/> Anything not associated with a search field is interpreted as a search in the card name, so <a href="/?q=l%3A4+utopia"><code>l:4 utopia</code></a> will show all level/rank 4 monsters with “Utopia” in their name.<br/>
If your search contains spaces (e.g. searching for an effect that says “destroy that target”), the text must be quoted like <code>effect:"destroy that target"</code>. If your search contains spaces (e.g. searching for an effect that says “destroy that target”), the text must be quoted like <code>effect:"destroy that target"</code>.
<br/><br/> <br/><br/>
Note that all fields are case-insensitive, so <code>class:NORMAL</code> is the same as <code>class:Normal</code> or <code>class:normal</code>. Note that all fields are case-insensitive, so <code>class:NORMAL</code> is the same as <code>class:Normal</code> or <code>class:normal</code>.
<br/>
<br/>
<h2>Search operators</h2> <h2>Search operators</h2>
The following search operators are supported: The following search operators are supported:

@ -69,7 +69,7 @@ h2 {
margin-block-end: 0; margin-block-end: 0;
margin-block-start: 0; margin-block-start: 0;
font-weight: normal; font-weight: normal;
font-size: 120%; font-size: 130%;
} }
.thumb { .thumb {