refactor page rendering to use an intermediate object
This commit is contained in:
parent
c89f48e769
commit
556bbb3a08
100
src/main.rs
100
src/main.rs
@ -11,6 +11,8 @@ mod data;
|
||||
mod filter;
|
||||
mod parser;
|
||||
|
||||
type AnyResult<T> = Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
// The yearly tins have ~250 cards in them.
|
||||
// I want to be higher than that so the page is usable as a set list.
|
||||
const RESULT_LIMIT: usize = 300;
|
||||
@ -55,6 +57,13 @@ struct Query {
|
||||
q: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PageData {
|
||||
title: String,
|
||||
query: Option<String>,
|
||||
body: String,
|
||||
}
|
||||
|
||||
const HEADER: &str = include_str!("../static/header.html");
|
||||
const HELP_CONTENT: &str = include_str!("../static/help.html");
|
||||
const FOOTER: &str = r#"<div id="bottom">
|
||||
@ -65,7 +74,7 @@ const FOOTER: &str = r#"<div id="bottom">
|
||||
</body></html>"#;
|
||||
|
||||
#[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 {
|
||||
Some(Either::Left(web::Query(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());
|
||||
let mut res = String::with_capacity(10_000);
|
||||
res.push_str(HEADER);
|
||||
render_searchbox(&mut res, &q)?;
|
||||
match q {
|
||||
Some(q) => render_results(&mut res, &q)?,
|
||||
None => res.push_str("Enter a query above to search"),
|
||||
}
|
||||
finish_document(&mut res);
|
||||
let data = match q {
|
||||
Some(q) => compute_results(q)?,
|
||||
None => PageData { title: "YGO card search".to_owned(), query: None, body: "Enter a query above to search".to_owned() },
|
||||
};
|
||||
add_data(&mut res, &data)?;
|
||||
Ok(HttpResponse::Ok().insert_header(header::ContentType::html()).body(res))
|
||||
}
|
||||
|
||||
#[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);
|
||||
res.push_str(HEADER);
|
||||
render_searchbox(&mut res, &None)?;
|
||||
match CARDS_BY_ID.get(&card_id) {
|
||||
Some(card) => {
|
||||
res.push_str(r#""#);
|
||||
write!(
|
||||
res,
|
||||
r#"
|
||||
<div>
|
||||
<img class="fullimage" src="{}/static/full/{}.jpg"/>
|
||||
{card}
|
||||
{}
|
||||
</div>"#,
|
||||
let data = match CARDS_BY_ID.get(&card_id) {
|
||||
Some(card) => PageData {
|
||||
title: format!("{} - YGO Card Database", card.name),
|
||||
query: None,
|
||||
body: format!(
|
||||
r#"<div><img class="fullimage" src="{}/static/full/{}.jpg"/>{card}{}</div>"#,
|
||||
IMG_HOST.as_str(),
|
||||
card.id,
|
||||
card.extended_info().unwrap_or_else(|_| String::new()),
|
||||
)?;
|
||||
}
|
||||
None => res.push_str("Card not found"),
|
||||
}
|
||||
finish_document(&mut res);
|
||||
),
|
||||
},
|
||||
None => PageData { title: "Card not found - YGO Card Database".to_owned(), query: None, body: "Card not found".to_owned() },
|
||||
};
|
||||
add_data(&mut res, &data)?;
|
||||
Ok(HttpResponse::Ok().insert_header(header::ContentType::html()).body(res))
|
||||
}
|
||||
|
||||
#[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);
|
||||
res.push_str(HEADER);
|
||||
render_searchbox(&mut res, &None)?;
|
||||
res.push_str(HELP_CONTENT);
|
||||
res.push_str(FOOTER);
|
||||
let data = PageData { query: None, title: "Query Syntax - YGO Card Database".to_owned(), body: HELP_CONTENT.to_owned() };
|
||||
add_data(&mut res, &data)?;
|
||||
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!(
|
||||
res,
|
||||
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>> {
|
||||
let (raw_filters, query) = match parser::parse_filters(query) {
|
||||
fn compute_results(raw_query: String) -> AnyResult<PageData> {
|
||||
let mut body = String::with_capacity(10_000);
|
||||
let (raw_filters, query) = match parser::parse_filters(&raw_query) {
|
||||
Ok(q) => q,
|
||||
Err(e) => {
|
||||
write!(res, "Could not parse query: {e:?}")?;
|
||||
return Ok(());
|
||||
let s = format!("Could not parse query: {e:?}");
|
||||
return Ok(PageData { title: s.clone(), query: Some(raw_query), body: s });
|
||||
}
|
||||
};
|
||||
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())
|
||||
.take(RESULT_LIMIT)
|
||||
.collect();
|
||||
write!(
|
||||
res,
|
||||
"<span class=\"meta\">Showing {} results where {} (took {:?})</span>",
|
||||
matches.len(),
|
||||
raw_filters.iter().map(|f| f.to_string()).join(" and "),
|
||||
now.elapsed()
|
||||
)?;
|
||||
let readable_query = format!("Showing {} results where {}", matches.len(), raw_filters.iter().map(|f| f.to_string()).join(" and "),);
|
||||
write!(body, "<span class=\"meta\">{readable_query} (took {:?})</span>", now.elapsed())?;
|
||||
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 {
|
||||
write!(
|
||||
res,
|
||||
body,
|
||||
r#"<a class="cardresult" href="/card/{}"><img src="{}/static/thumb/{}.jpg" class="thumb"/>{card}</a>"#,
|
||||
card.id,
|
||||
IMG_HOST.as_str(),
|
||||
card.id
|
||||
)?;
|
||||
}
|
||||
res.push_str("</div>");
|
||||
Ok(())
|
||||
body.push_str("</div>");
|
||||
Ok(PageData { title: readable_query.clone(), query: Some(raw_query), body })
|
||||
}
|
||||
|
||||
fn finish_document(res: &mut String) {
|
||||
res.push_str(FOOTER)
|
||||
fn add_data(res: &mut String, pd: &PageData) -> AnyResult<()> {
|
||||
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>
|
||||
<head>
|
||||
<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>
|
||||
<body>
|
||||
|
@ -1,6 +1,7 @@
|
||||
<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/>
|
||||
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>
|
||||
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>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>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>
|
||||
</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/>
|
||||
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/>
|
||||
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>
|
||||
The following search operators are supported:
|
||||
|
@ -69,7 +69,7 @@ h2 {
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
font-weight: normal;
|
||||
font-size: 120%;
|
||||
font-size: 130%;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
|
Loading…
Reference in New Issue
Block a user