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 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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user