diff --git a/Cargo.lock b/Cargo.lock
index b7ca2ff..d4702fa 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -743,15 +743,6 @@ dependencies = [
"unicode-segmentation",
]
-[[package]]
-name = "html-escape"
-version = "0.2.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
-dependencies = [
- "utf8-width",
-]
-
[[package]]
name = "html5ever"
version = "0.26.0"
@@ -1057,6 +1048,28 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
+[[package]]
+name = "maud"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df518b75016b4289cdddffa1b01f2122f4a49802c93191f3133f6dc2472ebcaa"
+dependencies = [
+ "itoa",
+ "maud_macros",
+]
+
+[[package]]
+name = "maud_macros"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa453238ec218da0af6b11fc5978d3b5c3a45ed97b722391a2a11f3306274e18"
+dependencies = [
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
[[package]]
name = "memchr"
version = "2.7.1"
@@ -1077,7 +1090,7 @@ dependencies = [
"eyre",
"fend-core",
"futures",
- "html-escape",
+ "maud",
"numbat",
"once_cell",
"rand",
@@ -1434,6 +1447,29 @@ dependencies = [
"ryu_floating_decimal",
]
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
[[package]]
name = "proc-macro2"
version = "1.0.79"
@@ -2337,12 +2373,6 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
-[[package]]
-name = "utf8-width"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
-
[[package]]
name = "valuable"
version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index b63c34e..7008f32 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,7 +23,7 @@ chrono-tz = { version = "0.8.6", features = ["case-insensitive"] }
eyre = "0.6.12"
fend-core = "1.4.5"
futures = "0.3.30"
-html-escape = "0.2.13"
+maud = "0.26.0"
numbat = "1.11.0"
once_cell = "1.19.0"
rand = "0.8.5"
diff --git a/README b/README
index 22a2f0b..3c15186 100644
--- a/README
+++ b/README
@@ -2,8 +2,7 @@ a cute metasearch engine
it sources from google, bing, brave, and a few others.
-it's written in rust using no templating engine and with as little client-side
-javascript as possible.
+it's written in rust, using as little client-side javascript as possible.
there's a demo instance at https://s.matdoes.dev, but don't use it as your
default or rely on it, please (so i don't get ratelimited by google).
diff --git a/src/engines/answer/dictionary.rs b/src/engines/answer/dictionary.rs
index db432dd..34efcbe 100644
--- a/src/engines/answer/dictionary.rs
+++ b/src/engines/answer/dictionary.rs
@@ -1,6 +1,7 @@
use std::collections::HashMap;
use eyre::eyre;
+use maud::{html, PreEscaped};
use serde::Deserialize;
use url::Url;
@@ -67,18 +68,10 @@ pub fn parse_response(
let word = key_to_title(mediawiki_key);
- let mut html = String::new();
-
let Some(entries) = res.0.get("en") else {
return Ok(EngineResponse::new());
};
- html.push_str(&format!(
- "
",
- mediawiki_key = html_escape::encode_safe(mediawiki_key),
- word = html_escape::encode_safe(&word),
- ));
-
let mut cleaner = ammonia::Builder::default();
cleaner
.link_rel(None)
@@ -86,11 +79,28 @@ pub fn parse_response(
Url::parse("https://en.wiktionary.org").unwrap(),
));
+ let mut html = String::new();
+
+ html.push_str(
+ &html! {
+ h2."answer-dictionary-word" {
+ a href={ "https://en.wiktionary.org/wiki/" (mediawiki_key) } {
+ (word)
+ }
+ }
+ }
+ .into_string(),
+ );
+
for entry in entries {
- html.push_str(&format!(
- "{part_of_speech} ",
- part_of_speech = html_escape::encode_safe(&entry.part_of_speech.to_lowercase())
- ));
+ html.push_str(
+ &html! {
+ span."answer-dictionary-part-of-speech" {
+ (entry.part_of_speech.to_lowercase())
+ }
+ }
+ .into_string(),
+ );
html.push_str("");
let mut previous_definitions = Vec::::new();
@@ -113,12 +123,19 @@ pub fn parse_response(
.clean(&definition.definition.replace('“', "\""))
.to_string();
- html.push_str(&format!("{definition_html}
"));
+ html.push_str(&html! { p { (PreEscaped(definition_html)) } }.into_string());
if !definition.examples.is_empty() {
for example in &definition.examples {
let example_html = cleaner.clean(example).to_string();
- html.push_str(&format!("{example_html} "));
+ html.push_str(
+ &html! {
+ blockquote."answer-dictionary-example" {
+ (PreEscaped(example_html))
+ }
+ }
+ .into_string(),
+ );
}
}
html.push_str("");
@@ -126,7 +143,7 @@ pub fn parse_response(
html.push_str(" ");
}
- Ok(EngineResponse::answer_html(html))
+ Ok(EngineResponse::answer_html(PreEscaped(html)))
}
fn key_to_title(key: &str) -> String {
diff --git a/src/engines/answer/fend.rs b/src/engines/answer/fend.rs
index 2709d27..99e8e33 100644
--- a/src/engines/answer/fend.rs
+++ b/src/engines/answer/fend.rs
@@ -1,6 +1,7 @@
use std::cell::Cell;
use fend_core::SpanKind;
+use maud::{html, PreEscaped};
use once_cell::sync::Lazy;
use crate::engines::EngineResponse;
@@ -10,15 +11,14 @@ use super::regex;
pub fn request(query: &str) -> EngineResponse {
let query = clean_query(query);
- let Some(result_html) = evaluate(&query, true) else {
+ let Some(result_html) = evaluate_to_html(&query, true) else {
return EngineResponse::new();
};
- EngineResponse::answer_html(format!(
- r#"{query} =
-{result_html} "#,
- query = html_escape::encode_safe(&query),
- ))
+ EngineResponse::answer_html(html! {
+ p."answer-query" { (query) " =" }
+ h3 { b { (result_html) } }
+ })
}
pub fn request_autocomplete(query: &str) -> Vec {
@@ -26,7 +26,7 @@ pub fn request_autocomplete(query: &str) -> Vec {
let query = clean_query(query);
- if let Some(result) = evaluate(&query, false) {
+ if let Some(result) = evaluate_to_plaintext(&query, false) {
results.push(format!("= {result}"));
}
@@ -43,20 +43,24 @@ pub struct Span {
pub kind: SpanKind,
}
-fn evaluate(query: &str, html: bool) -> Option {
+fn evaluate_to_plaintext(query: &str, html: bool) -> Option {
let spans = evaluate_into_spans(query, html);
-
if spans.is_empty() {
return None;
}
- if !html {
- return Some(
- spans
- .iter()
- .map(|span| span.text.clone())
- .collect::(),
- );
+ return Some(
+ spans
+ .iter()
+ .map(|span| span.text.clone())
+ .collect::(),
+ );
+}
+
+fn evaluate_to_html(query: &str, html: bool) -> Option> {
+ let spans = evaluate_into_spans(query, html);
+ if spans.is_empty() {
+ return None;
}
let mut result_html = String::new();
@@ -69,12 +73,16 @@ fn evaluate(query: &str, html: bool) -> Option {
_ => "",
};
if class.is_empty() {
- result_html.push_str(&html_escape::encode_safe(&span.text));
+ result_html.push_str(&html! { (span.text) }.into_string());
} else {
- result_html.push_str(&format!(
- r#"{text} "#,
- text = html_escape::encode_safe(&span.text)
- ));
+ result_html.push_str(
+ &html! {
+ span.(class) {
+ (span.text)
+ }
+ }
+ .into_string(),
+ );
}
}
@@ -86,11 +94,16 @@ fn evaluate(query: &str, html: bool) -> Option {
{
let hex = spans[0].text.trim_start_matches("0x");
if let Ok(num) = u64::from_str_radix(hex, 16) {
- result_html.push_str(&format!(r#" "#));
+ result_html.push_str(
+ &html! {
+ span."answer-comment" { " = " (num) }
+ }
+ .into_string(),
+ );
}
}
- Some(result_html)
+ Some(PreEscaped(result_html))
}
pub static FEND_CONTEXT: Lazy = Lazy::new(|| {
diff --git a/src/engines/answer/ip.rs b/src/engines/answer/ip.rs
index 52d13f6..88cde5f 100644
--- a/src/engines/answer/ip.rs
+++ b/src/engines/answer/ip.rs
@@ -1,3 +1,5 @@
+use maud::html;
+
use crate::engines::{EngineResponse, SearchQuery};
use super::regex;
@@ -9,8 +11,7 @@ pub fn request(query: &SearchQuery) -> EngineResponse {
let ip = &query.ip;
- EngineResponse::answer_html(format!(
- r#"{ip} "#,
- ip = html_escape::encode_safe(ip)
- ))
+ EngineResponse::answer_html(html! {
+ h3 { b { (ip) } }
+ })
}
diff --git a/src/engines/answer/notepad.rs b/src/engines/answer/notepad.rs
index 425dc12..9f75a89 100644
--- a/src/engines/answer/notepad.rs
+++ b/src/engines/answer/notepad.rs
@@ -1,3 +1,5 @@
+use maud::html;
+
use crate::engines::{EngineResponse, SearchQuery};
use super::regex;
@@ -10,5 +12,7 @@ pub fn request(query: &SearchQuery) -> EngineResponse {
// This allows pasting styles which is undesired behavior, and the
// `contenteditable="plaintext-only"` attribute currently only works on Chrome.
// This should be updated when the attribute becomes available in more browsers
- EngineResponse::answer_html(r#"
"#.to_string())
+ EngineResponse::answer_html(html! {
+ div."answer-notepad" contenteditable="plaintext-only" {}
+ })
}
diff --git a/src/engines/answer/numbat.rs b/src/engines/answer/numbat.rs
index 2d352d9..c429b51 100644
--- a/src/engines/answer/numbat.rs
+++ b/src/engines/answer/numbat.rs
@@ -1,6 +1,7 @@
use std::collections::HashSet;
use fend_core::SpanKind;
+use maud::{html, PreEscaped};
use numbat::{
markup::{FormatType, FormattedString, Markup},
pretty_print::PrettyPrint,
@@ -23,10 +24,10 @@ pub fn request(query: &str) -> EngineResponse {
return EngineResponse::new();
};
- EngineResponse::answer_html(format!(
- r#"{query_html} =
-{result_html} "#
- ))
+ EngineResponse::answer_html(html! {
+ p."answer-query" { (query_html) " =" }
+ h3 { b { (result_html) } }
+ })
}
pub fn request_autocomplete(query: &str) -> Vec {
@@ -119,8 +120,8 @@ fn evaluate_for_autocomplete(query: &str) -> Option {
}
pub struct NumbatResponse {
- pub query_html: String,
- pub result_html: String,
+ pub query_html: PreEscaped,
+ pub result_html: PreEscaped,
}
fn evaluate(query: &str) -> Option {
@@ -155,7 +156,7 @@ fn fix_markup(markup: Markup) -> Markup {
Markup(reordered_markup)
}
-fn markup_to_html(markup: Markup) -> String {
+fn markup_to_html(markup: Markup) -> PreEscaped {
let mut html = String::new();
for FormattedString(_, format_type, content) in markup.0 {
let class = match format_type {
@@ -165,15 +166,17 @@ fn markup_to_html(markup: Markup) -> String {
_ => "",
};
if class.is_empty() {
- html.push_str(&html_escape::encode_safe(&content));
+ html.push_str(&html! {(content)}.into_string());
} else {
- html.push_str(&format!(
- r#"{content} "#,
- content = html_escape::encode_safe(&content)
- ));
+ html.push_str(
+ &html! {
+ span.(class) { (content) }
+ }
+ .into_string(),
+ );
}
}
- html
+ PreEscaped(html)
}
pub static NUMBAT_CTX: Lazy = Lazy::new(|| {
diff --git a/src/engines/answer/thesaurus.rs b/src/engines/answer/thesaurus.rs
index 139dfd6..2f3ac71 100644
--- a/src/engines/answer/thesaurus.rs
+++ b/src/engines/answer/thesaurus.rs
@@ -1,4 +1,5 @@
use eyre::eyre;
+use maud::{html, PreEscaped};
use scraper::{Html, Selector};
use serde::Deserialize;
use tracing::error;
@@ -149,23 +150,24 @@ fn parse_thesaurus_com_item(
})
}
-fn render_thesaurus_html(ThesaurusResponse { word, items }: ThesaurusResponse) -> String {
- let mut html = String::new();
+fn render_thesaurus_html(
+ ThesaurusResponse { word, items }: ThesaurusResponse,
+) -> PreEscaped {
+ html! {
+ h2."answer-thesaurus-word" {
+ a href={ "https://www.thesaurus.com/browse/" (word) } {
+ (word)
+ }
+ }
+ div."answer-thesaurus-items" {
+ @for item in items {
+ div."answer-thesaurus-item" {
+ (render_thesaurus_item_html(item))
+ }
+ }
+ }
- html.push_str(&format!(
- "",
- word = html_escape::encode_safe(&word)
- ));
-
- html.push_str("");
- for item in items {
- html.push_str("
");
- html.push_str(&render_thesaurus_item_html(item));
- html.push_str("
");
}
- html.push_str("
");
-
- html
}
fn render_thesaurus_item_html(
@@ -176,53 +178,46 @@ fn render_thesaurus_item_html(
strong_matches,
weak_matches,
}: ThesaurusItem,
-) -> String {
+) -> PreEscaped {
let mut html = String::new();
- html.push_str(&format!(
- "{part_of_speech} , as in {as_in} ",
- part_of_speech = html_escape::encode_safe(&part_of_speech.to_lowercase()),
- as_in = html_escape::encode_safe(&as_in)
- ));
+ html.push_str(
+ &html! {
+ span."answer-thesaurus-word-description" {
+ span."answer-thesaurus-part-of-speech" { (part_of_speech.to_lowercase()) }
+ ", as in "
+ span."answer-thesaurus-as-in" { (as_in) }
+ }
+ }
+ .into_string(),
+ );
let render_matches = |matches: Vec, strength: &str| {
if matches.is_empty() {
- return String::new();
+ return PreEscaped::default();
}
- let mut html = String::new();
-
- html.push_str(&format!(
- "",
- strength_id = html_escape::encode_safe(&strength.to_lowercase().replace(' ', "-"))
- ));
-
- html.push_str(&format!(
- "
{strength} {match_or_matches} ",
- strength = html_escape::encode_safe(&strength),
- match_or_matches = if matches.len() == 1 {
- "match"
- } else {
- "matches"
+ html! {
+ div.{ "answer-thesaurus-" (strength.to_lowercase().replace(' ', "-")) } {
+ h3."answer-thesaurus-category-title" {
+ (strength)
+ " "
+ (if matches.len() == 1 { "match" } else { "matches" })
+ }
+ ul."answer-thesaurus-list" {
+ @for synonym in matches {
+ li {
+ a href={ "https://www.thesaurus.com/browse/" (synonym) } { (synonym) }
+ }
+ }
+ }
}
- ));
- html.push_str("
");
- for synonym in matches {
- html.push_str(&format!(
- "{synonym} ",
- synonym = html_escape::encode_safe(&synonym)
- ));
}
- html.push_str(" ");
-
- html.push_str("
");
-
- html
};
- html.push_str(&render_matches(strongest_matches, "Strongest"));
- html.push_str(&render_matches(strong_matches, "Strong"));
- html.push_str(&render_matches(weak_matches, "Weak"));
+ html.push_str(&render_matches(strongest_matches, "Strongest").into_string());
+ html.push_str(&render_matches(strong_matches, "Strong").into_string());
+ html.push_str(&render_matches(weak_matches, "Weak").into_string());
- html
+ PreEscaped(html)
}
diff --git a/src/engines/answer/timezone.rs b/src/engines/answer/timezone.rs
index a845336..9f3c413 100644
--- a/src/engines/answer/timezone.rs
+++ b/src/engines/answer/timezone.rs
@@ -1,5 +1,6 @@
use chrono::{DateTime, TimeZone};
use chrono_tz::{OffsetComponents, Tz};
+use maud::html;
use crate::engines::EngineResponse;
@@ -8,13 +9,15 @@ use super::regex;
pub fn request(query: &str) -> EngineResponse {
match evaluate(query) {
None => EngineResponse::new(),
- Some(TimeResponse::Current { time, timezone }) => EngineResponse::answer_html(format!(
- r#"Current time in {timezone}
-{time} "#,
- time = html_escape::encode_safe(&time.format("%-I:%M %P").to_string()),
- date = html_escape::encode_safe(&time.format("%B %-d").to_string()),
- timezone = html_escape::encode_safe(&timezone_to_string(timezone)),
- )),
+ Some(TimeResponse::Current { time, timezone }) => EngineResponse::answer_html(html! {
+ p."answer-query" { "Current time in " (timezone_to_string(timezone)) }
+ h3 {
+ b { (time.format("%-I:%M %P")) }
+ span."answer-comment" {
+ "(" (time.format("%B %-d")) ")"
+ }
+ }
+ }),
Some(TimeResponse::Conversion {
source_timezone,
target_timezone,
@@ -22,22 +25,31 @@ pub fn request(query: &str) -> EngineResponse {
target_time,
source_offset,
target_offset,
- }) => EngineResponse::answer_html(format!(
- r#"{source_time} {source_timezone} to {target_timezone}
-{target_time} "#,
- source_time = html_escape::encode_safe(&source_time.format("%-I:%M %P").to_string()),
- target_time = html_escape::encode_safe(&target_time.format("%-I:%M %P").to_string()),
- source_timezone = html_escape::encode_safe(&timezone_to_string(source_timezone)),
- target_timezone = html_escape::encode_safe(&timezone_to_string(target_timezone)),
- delta = html_escape::encode_safe(&{
- let delta_minutes = (target_offset - source_offset).num_minutes();
- if delta_minutes % 60 == 0 {
- format!("{:+}", delta_minutes / 60)
- } else {
- format!("{:+}:{}", delta_minutes / 60, delta_minutes % 60)
+ }) => {
+ let delta_minutes = (target_offset - source_offset).num_minutes();
+ let delta = if delta_minutes % 60 == 0 {
+ format!("{:+}", delta_minutes / 60)
+ } else {
+ format!("{:+}:{}", delta_minutes / 60, delta_minutes % 60)
+ };
+
+ EngineResponse::answer_html(html! {
+ p."answer-query" {
+ (source_time.format("%-I:%M %P"))
+ " "
+ (timezone_to_string(source_timezone))
+ " to "
+ (timezone_to_string(target_timezone))
+ }
+ h3 {
+ b { (target_time.format("%-I:%M %P")) }
+ " "
+ span."answer-comment" {
+ (timezone_to_string(target_timezone)) " (" (delta) ")"
+ }
}
})
- )),
+ }
}
}
diff --git a/src/engines/answer/useragent.rs b/src/engines/answer/useragent.rs
index ee9418c..9cc6583 100644
--- a/src/engines/answer/useragent.rs
+++ b/src/engines/answer/useragent.rs
@@ -1,3 +1,5 @@
+use maud::html;
+
use crate::engines::{EngineResponse, SearchQuery};
use super::regex;
@@ -12,11 +14,10 @@ pub fn request(query: &SearchQuery) -> EngineResponse {
let user_agent = query.request_headers.get("user-agent");
EngineResponse::answer_html(if let Some(user_agent) = user_agent {
- format!(
- "{user_agent} ",
- user_agent = html_escape::encode_safe(user_agent)
- )
+ html! {
+ h3 { b { (user_agent) } }
+ }
} else {
- "You don't have a user agent".to_string()
+ html! { "You don't have a user agent" }
})
}
diff --git a/src/engines/answer/wikipedia.rs b/src/engines/answer/wikipedia.rs
index e956683..ed6ff7b 100644
--- a/src/engines/answer/wikipedia.rs
+++ b/src/engines/answer/wikipedia.rs
@@ -1,5 +1,6 @@
use std::collections::HashMap;
+use maud::html;
use serde::Deserialize;
use url::Url;
@@ -89,10 +90,10 @@ pub fn parse_response(body: &str) -> eyre::Result {
let page_title = title.replace(' ', "_");
let page_url = format!("https://en.wikipedia.org/wiki/{page_title}");
- Ok(EngineResponse::infobox_html(format!(
- r#"{title} {extract}
"#,
- page_url = html_escape::encode_quoted_attribute(&page_url),
- title = html_escape::encode_safe(title),
- extract = html_escape::encode_safe(&extract),
- )))
+ Ok(EngineResponse::infobox_html(html! {
+ a href=(page_url) {
+ h2 { (title) }
+ }
+ p { (extract) }
+ }))
}
diff --git a/src/engines/macros.rs b/src/engines/macros.rs
index c8887ba..2f7184d 100644
--- a/src/engines/macros.rs
+++ b/src/engines/macros.rs
@@ -115,7 +115,7 @@ macro_rules! engine_postsearch_requests {
}
#[must_use]
- pub fn postsearch_parse_response(&self, res: &HttpResponse) -> Option {
+ pub fn postsearch_parse_response(&self, res: &HttpResponse) -> Option> {
match self {
$(
Engine::$engine => $crate::engine_parse_response! { res, $module::$engine_id::$parse_response }?,
diff --git a/src/engines/mod.rs b/src/engines/mod.rs
index 10691dd..7a7e123 100644
--- a/src/engines/mod.rs
+++ b/src/engines/mod.rs
@@ -9,6 +9,7 @@ use std::{
};
use futures::future::join_all;
+use maud::PreEscaped;
use once_cell::sync::Lazy;
use reqwest::header::HeaderMap;
use serde::{Deserialize, Deserializer};
@@ -189,8 +190,8 @@ pub struct EngineFeaturedSnippet {
pub struct EngineResponse {
pub search_results: Vec,
pub featured_snippet: Option,
- pub answer_html: Option,
- pub infobox_html: Option,
+ pub answer_html: Option>,
+ pub infobox_html: Option>,
}
impl EngineResponse {
@@ -200,7 +201,7 @@ impl EngineResponse {
}
#[must_use]
- pub fn answer_html(html: String) -> Self {
+ pub fn answer_html(html: PreEscaped) -> Self {
Self {
answer_html: Some(html),
..Default::default()
@@ -208,7 +209,7 @@ impl EngineResponse {
}
#[must_use]
- pub fn infobox_html(html: String) -> Self {
+ pub fn infobox_html(html: PreEscaped) -> Self {
Self {
infobox_html: Some(html),
..Default::default()
@@ -497,13 +498,13 @@ pub struct FeaturedSnippet {
#[derive(Debug, Clone)]
pub struct Answer {
- pub html: String,
+ pub html: PreEscaped,
pub engine: Engine,
}
#[derive(Debug, Clone)]
pub struct Infobox {
- pub html: String,
+ pub html: PreEscaped,
pub engine: Engine,
}
diff --git a/src/engines/postsearch/docs_rs.rs b/src/engines/postsearch/docs_rs.rs
index fcbea7e..2c0090e 100644
--- a/src/engines/postsearch/docs_rs.rs
+++ b/src/engines/postsearch/docs_rs.rs
@@ -1,3 +1,4 @@
+use maud::{html, PreEscaped};
use scraper::{Html, Selector};
use crate::engines::{HttpResponse, Response, CLIENT};
@@ -12,7 +13,7 @@ pub fn request(response: &Response) -> Option {
None
}
-pub fn parse_response(HttpResponse { res, body, .. }: &HttpResponse) -> Option {
+pub fn parse_response(HttpResponse { res, body, .. }: &HttpResponse) -> Option> {
let url = res.url().clone();
let dom = Html::parse_document(body);
@@ -53,22 +54,21 @@ pub fn parse_response(HttpResponse { res, body, .. }: &HttpResponse) -> Option{category} {title} {version} "#,
- url = html_escape::encode_quoted_attribute(&url.to_string()),
- title = html_escape::encode_safe(&title),
- version = html_escape::encode_safe(&version),
- )
- } else {
- format!(
- r#""#,
- url = html_escape::encode_quoted_attribute(&url.to_string()),
- title = html_escape::encode_safe(&title),
- )
+ let title_html = html! {
+ h2 {
+ (category)
+ " "
+ a href=(url) { (title) }
+ @if category == "Crate" {
+ span."infobox-docs_rs-version" { (version) }
+ }
+ }
};
- Some(format!(
- r#"{title_html} {doc_html}
"#
- ))
+ Some(html! {
+ (title_html)
+ div."infobox-docs_rs-doc" {
+ (PreEscaped(doc_html))
+ }
+ })
}
diff --git a/src/engines/postsearch/github.rs b/src/engines/postsearch/github.rs
index 08d3ba5..f2ef363 100644
--- a/src/engines/postsearch/github.rs
+++ b/src/engines/postsearch/github.rs
@@ -1,3 +1,4 @@
+use maud::{html, PreEscaped};
use scraper::{Html, Selector};
use url::Url;
@@ -13,7 +14,7 @@ pub fn request(response: &Response) -> Option {
None
}
-pub fn parse_response(body: &str) -> Option {
+pub fn parse_response(body: &str) -> Option> {
let dom = Html::parse_document(body);
let url_relative = dom
@@ -68,10 +69,12 @@ pub fn parse_response(body: &str) -> Option {
.collect::()
};
- Some(format!(
- r#"{title}
-{readme_html}
"#,
- url = html_escape::encode_quoted_attribute(&url),
- title = html_escape::encode_safe(&title),
- ))
+ Some(html! {
+ a href=(url) {
+ h1 { (title) }
+ }
+ div."infobox-github-readme" {
+ (PreEscaped(readme_html))
+ }
+ })
}
diff --git a/src/engines/postsearch/mdn.rs b/src/engines/postsearch/mdn.rs
index 99219bf..1736b6c 100644
--- a/src/engines/postsearch/mdn.rs
+++ b/src/engines/postsearch/mdn.rs
@@ -1,3 +1,4 @@
+use maud::{html, PreEscaped};
use scraper::{Html, Selector};
use serde::Deserialize;
use tracing::error;
@@ -22,7 +23,9 @@ pub fn request(response: &Response) -> Option {
None
}
-pub fn parse_response(HttpResponse { res, body, config }: &HttpResponse) -> Option {
+pub fn parse_response(
+ HttpResponse { res, body, config }: &HttpResponse,
+) -> Option> {
let config_toml = config.engines.get(Engine::Mdn).extra.clone();
let config: MdnConfig = match toml::Value::Table(config_toml).try_into() {
Ok(args) => args,
@@ -57,7 +60,7 @@ pub fn parse_response(HttpResponse { res, body, config }: &HttpResponse) -> Opti
.map(|doc| doc.inner_html())
.take(max_sections)
.collect::>()
- .join(" ");
+ .join(" ");
let doc_html = ammonia::Builder::default()
.link_rel(None)
@@ -65,13 +68,16 @@ pub fn parse_response(HttpResponse { res, body, config }: &HttpResponse) -> Opti
.clean(&doc_html)
.to_string();
- let title_html = format!(
- r#""#,
- url = html_escape::encode_quoted_attribute(&url.to_string()),
- title = html_escape::encode_safe(&page_title),
- );
+ let title_html = html! {
+ h2 {
+ a href=(url) { (page_title) }
+ }
+ };
- Some(format!(
- r#"{title_html}{doc_html}
"#
- ))
+ Some(html! {
+ (title_html)
+ div."infobox-mdn-article" {
+ (PreEscaped(doc_html))
+ }
+ })
}
diff --git a/src/engines/postsearch/minecraft_wiki.rs b/src/engines/postsearch/minecraft_wiki.rs
index 854857f..b29276c 100644
--- a/src/engines/postsearch/minecraft_wiki.rs
+++ b/src/engines/postsearch/minecraft_wiki.rs
@@ -1,3 +1,4 @@
+use maud::{html, PreEscaped};
use scraper::{Html, Selector};
use crate::engines::{HttpResponse, Response, CLIENT};
@@ -12,7 +13,7 @@ pub fn request(response: &Response) -> Option {
None
}
-pub fn parse_response(HttpResponse { res, body, .. }: &HttpResponse) -> Option {
+pub fn parse_response(HttpResponse { res, body, .. }: &HttpResponse) -> Option> {
let url = res.url().clone();
let dom = Html::parse_document(body);
@@ -41,13 +42,16 @@ pub fn parse_response(HttpResponse { res, body, .. }: &HttpResponse) -> Option{title} "#,
- url = html_escape::encode_quoted_attribute(&url.to_string()),
- title = html_escape::encode_safe(&page_title),
- );
+ let title_html = html! {
+ h2 {
+ a href=(url) { (page_title) }
+ }
+ };
- Some(format!(
- r#"{title_html}{doc_html}
"#
- ))
+ Some(html! {
+ (title_html)
+ div."infobox-minecraft_wiki-article" {
+ (PreEscaped(doc_html))
+ }
+ })
}
diff --git a/src/engines/postsearch/stackexchange.rs b/src/engines/postsearch/stackexchange.rs
index 6ed8655..f576b81 100644
--- a/src/engines/postsearch/stackexchange.rs
+++ b/src/engines/postsearch/stackexchange.rs
@@ -1,3 +1,4 @@
+use maud::{html, PreEscaped};
use scraper::{Html, Selector};
use url::Url;
@@ -15,7 +16,7 @@ pub fn request(response: &Response) -> Option {
None
}
-pub fn parse_response(body: &str) -> Option {
+pub fn parse_response(body: &str) -> Option> {
let dom = Html::parse_document(body);
let title = dom
@@ -55,10 +56,12 @@ pub fn parse_response(body: &str) -> Option {
let url = format!("{url}#{answer_id}");
- Some(format!(
- r#"{title}
-{answer_html}
"#,
- url = html_escape::encode_quoted_attribute(&url.to_string()),
- title = html_escape::encode_safe(&title),
- ))
+ Some(html! {
+ a href=(url) {
+ h2 { (title) }
+ }
+ div."infobox-stackexchange-answer" {
+ (PreEscaped(answer_html))
+ }
+ })
}
diff --git a/src/web/assets/index.html b/src/web/assets/index.html
deleted file mode 100644
index 99864bd..0000000
--- a/src/web/assets/index.html
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
-
- metasearch
-
-
-
-
-
-
-
metasearch
-
-
- %version-info%
-
-
diff --git a/src/web/index.rs b/src/web/index.rs
new file mode 100644
index 0000000..af35b57
--- /dev/null
+++ b/src/web/index.rs
@@ -0,0 +1,58 @@
+use std::sync::Arc;
+
+use axum::{extract::State, http::header, response::IntoResponse};
+use maud::{html, PreEscaped, DOCTYPE};
+
+use crate::config::Config;
+
+const BASE_COMMIT_URL: &str = "https://github.com/mat-1/metasearch2/commit/";
+const VERSION: &str = std::env!("CARGO_PKG_VERSION");
+const COMMIT_HASH: &str = std::env!("GIT_HASH");
+const COMMIT_HASH_SHORT: &str = std::env!("GIT_HASH_SHORT");
+
+pub async fn index(State(config): State>) -> impl IntoResponse {
+ let mut html = String::new();
+ html.push_str(
+ &html! {
+ (PreEscaped("\n"))
+ (DOCTYPE)
+ html lang="en" {
+ head {
+ meta charset="UTF-8";
+ meta name="viewport" content="width=device-width, initial-scale=1.0";
+ title { "metasearch" }
+ link rel="stylesheet" href="/style.css";
+ script src="/script.js" defer {}
+ link rel="search" type="application/opensearchdescription+xml" title="metasearch" href="/opensearch.xml";
+ }
+ body {
+ div."main-container" {
+ h1 { "metasearch" }
+ form."search-form" action="/search" method="get" {
+ input type="text" name="q" placeholder="Search" id="search-input" autofocus onfocus="this.select()" autocomplete="off";
+ input type="submit" value="Search";
+ }
+ }
+ @if config.version_info.unwrap() {
+ span."version-info" {
+ @if COMMIT_HASH == "unknown" || COMMIT_HASH_SHORT == "unknown" {
+ "Version "
+ (VERSION)
+ } else {
+ "Version "
+ (VERSION)
+ " ("
+ a href=(format!("{BASE_COMMIT_URL}{COMMIT_HASH}")) { (COMMIT_HASH_SHORT) }
+ ")"
+ }
+ }
+ }
+ }
+
+ }
+ }
+ .into_string(),
+ );
+
+ ([(header::CONTENT_TYPE, "text/html; charset=utf-8")], html)
+}
diff --git a/src/web/mod.rs b/src/web/mod.rs
index 6d3d832..267a0c2 100644
--- a/src/web/mod.rs
+++ b/src/web/mod.rs
@@ -1,4 +1,5 @@
pub mod autocomplete;
+pub mod index;
pub mod opensearch;
pub mod search;
@@ -9,36 +10,11 @@ use tracing::info;
use crate::config::Config;
-const BASE_COMMIT_URL: &str = "https://github.com/mat-1/metasearch2/commit/";
-const VERSION: &str = std::env!("CARGO_PKG_VERSION");
-const COMMIT_HASH: &str = std::env!("GIT_HASH");
-const COMMIT_HASH_SHORT: &str = std::env!("GIT_HASH_SHORT");
-
pub async fn run(config: Config) {
let bind_addr = config.bind;
- let version_info = if config.version_info.unwrap() {
- if COMMIT_HASH == "unknown" || COMMIT_HASH_SHORT == "unknown" {
- format!(r#"Version {VERSION} (unknown commit) "#)
- } else {
- format!(
- r#"Version {VERSION} ({COMMIT_HASH_SHORT} ) "#
- )
- }
- } else {
- String::new()
- };
-
let app = Router::new()
- .route(
- "/",
- get(|| async move {
- (
- [(header::CONTENT_TYPE, "text/html; charset=utf-8")],
- include_str!("assets/index.html").replace("%version-info%", &version_info),
- )
- }),
- )
+ .route("/", get(index::index))
.route(
"/style.css",
get(|| async {
diff --git a/src/web/search.rs b/src/web/search.rs
index 5ebe4d4..8883c36 100644
--- a/src/web/search.rs
+++ b/src/web/search.rs
@@ -8,7 +8,7 @@ use axum::{
response::IntoResponse,
};
use bytes::Bytes;
-use html_escape::{encode_text, encode_unquoted_attribute};
+use maud::{html, PreEscaped};
use crate::{
config::Config,
@@ -16,28 +16,36 @@ use crate::{
};
fn render_beginning_of_html(query: &str) -> String {
+ let head_html = html! {
+ head {
+ meta charset="UTF-8";
+ meta name="viewport" content="width=device-width, initial-scale=1.0";
+ title {
+ (query)
+ " - metasearch"
+ }
+ link rel="stylesheet" href="/style.css";
+ script src="/script.js" defer {}
+ link rel="search" type="application/opensearchdescription+xml" title="metasearch" href="/opensearch.xml";
+ }
+ }.into_string();
+ let form_html = html! {
+ form."search-form" action="/search" method="get" {
+ input #"search-input" type="text" name="q" placeholder="Search" value=(query) autofocus onfocus="this.select()" autocomplete="off";
+ input type="submit" value="Search";
+ }
+ }.into_string();
+
format!(
r#"
-
-
-
- {} - metasearch
-
-
-
-
+{head_html}
-
+ {form_html}
-"#,
- encode_text(query),
- encode_unquoted_attribute(query)
+"#
)
}
@@ -45,7 +53,7 @@ fn render_end_of_html() -> String {
r"
".to_string()
}
-fn render_engine_list(engines: &[engines::Engine], config: &Config) -> String {
+fn render_engine_list(engines: &[engines::Engine], config: &Config) -> PreEscaped {
let mut html = String::new();
let mut first_iter = true;
for engine in engines {
@@ -59,85 +67,92 @@ fn render_engine_list(engines: &[engines::Engine], config: &Config) -> String {
} else {
raw_engine_id.to_string()
};
- html.push_str(&format!(
- r#"{engine} "#,
- engine = encode_text(&engine_id)
- ));
+ html.push_str(
+ &html! {
+ span."engine-list-item" {
+ (engine_id)
+ }
+ }
+ .into_string(),
+ )
+ }
+ html! {
+ div."engine-list" {
+ (PreEscaped(html))
+ }
}
- format!(r#"{html}
"#)
}
-fn render_search_result(result: &engines::SearchResult, config: &Config) -> String {
- format!(
- r#"
-"#,
- url_attr = encode_unquoted_attribute(&result.url),
- url = encode_text(&result.url),
- title = encode_text(&result.title),
- desc = encode_text(&result.description),
- engines_html =
- render_engine_list(&result.engines.iter().copied().collect::>(), config)
- )
+fn render_search_result(result: &engines::SearchResult, config: &Config) -> PreEscaped {
+ html! {
+ div."search-result" {
+ a."search-result-anchor" rel="noreferrer" href=(result.url) {
+ span."search-result-url" { (result.url) }
+ h3."search-result-title" { (result.title) }
+ }
+ p."search-result-description" { (result.description) }
+ (render_engine_list(&result.engines.iter().copied().collect::>(), config))
+ }
+ }
}
-fn render_featured_snippet(featured_snippet: &engines::FeaturedSnippet, config: &Config) -> String {
- format!(
- r#"
-"#,
- desc = encode_text(&featured_snippet.description),
- url_attr = encode_unquoted_attribute(&featured_snippet.url),
- url = encode_text(&featured_snippet.url),
- title = encode_text(&featured_snippet.title),
- engines_html = render_engine_list(&[featured_snippet.engine], config)
- )
+fn render_featured_snippet(
+ featured_snippet: &engines::FeaturedSnippet,
+ config: &Config,
+) -> PreEscaped {
+ html! {
+ div."featured-snippet" {
+ p."search-result-description" { (featured_snippet.description) }
+ a."search-result-anchor" rel="noreferrer" href=(featured_snippet.url) {
+ span."search-result-url" { (featured_snippet.url) }
+ h3."search-result-title" { (featured_snippet.title) }
+ }
+ (render_engine_list(&[featured_snippet.engine], config))
+ }
+ }
}
-fn render_results(response: Response) -> String {
+fn render_results(response: Response) -> PreEscaped {
let mut html = String::new();
if let Some(infobox) = &response.infobox {
- html.push_str(&format!(
- r#"{infobox_html}{engines_html}
"#,
- infobox_html = &infobox.html,
- engines_html = render_engine_list(&[infobox.engine], &response.config)
- ));
+ html.push_str(
+ &html! {
+ div."infobox" {
+ (infobox.html)
+ (render_engine_list(&[infobox.engine], &response.config))
+ }
+ }
+ .into_string(),
+ );
}
if let Some(answer) = &response.answer {
- html.push_str(&format!(
- r#"{answer_html}{engines_html}
"#,
- answer_html = &answer.html,
- engines_html = render_engine_list(&[answer.engine], &response.config)
- ));
+ html.push_str(
+ &html! {
+ div."answer" {
+ (answer.html)
+ (render_engine_list(&[answer.engine], &response.config))
+ }
+ }
+ .into_string(),
+ );
}
if let Some(featured_snippet) = &response.featured_snippet {
- html.push_str(&render_featured_snippet(featured_snippet, &response.config));
+ html.push_str(&render_featured_snippet(featured_snippet, &response.config).into_string());
}
for result in &response.search_results {
- html.push_str(&render_search_result(result, &response.config));
+ html.push_str(&render_search_result(result, &response.config).into_string());
}
- if response.infobox.is_none()
- && response.answer.is_none()
- && response.featured_snippet.is_none()
- && response.search_results.is_empty()
- {
- html.push_str(r#"No results.
"#);
+ if html.is_empty() {
+ html.push_str(
+ &html! {
+ p { "No results." }
+ }
+ .into_string(),
+ );
}
- html
+ PreEscaped(html)
}
fn render_engine_progress_update(
@@ -149,10 +164,22 @@ fn render_engine_progress_update(
EngineProgressUpdate::Requesting => "requesting",
EngineProgressUpdate::Downloading => "downloading",
EngineProgressUpdate::Parsing => "parsing",
- EngineProgressUpdate::Done => "done ",
+ EngineProgressUpdate::Done => {
+ &{ html! { span."progress-update-done" { "done" } }.into_string() }
+ }
};
- format!(r#"{time_ms:>4}ms {engine} {message}"#)
+ html! {
+ span."progress-update-time" {
+ (format!("{time_ms:>4}"))
+ "ms"
+ }
+ " "
+ (engine)
+ " "
+ (PreEscaped(message))
+ }
+ .into_string()
}
pub async fn route(
@@ -235,24 +262,27 @@ pub async fn route(
second_part.push_str(" "); // close progress-updates
second_part.push_str("");
- second_part.push_str(&render_results(results));
+ second_part.push_str(&render_results(results).into_string());
yield Ok(Bytes::from(second_part));
},
ProgressUpdateData::PostSearchInfobox(infobox) => {
- third_part.push_str(&format!(
- r#"{infobox_html}{engines_html}
"#,
- infobox_html = &infobox.html,
- engines_html = render_engine_list(&[infobox.engine], &config)
- ));
+ third_part.push_str(&html! {
+ div."infobox"."postsearch-infobox" {
+ (infobox.html)
+ (render_engine_list(&[infobox.engine], &config))
+ }
+ }.into_string());
}
}
}
if let Err(e) = search_future.await? {
- let error_html = format!(
- r#"Error: {}"#,
- encode_text(&e.to_string())
- );
+ let error_html = html! {
+ h1 {
+ "Error: "
+ (e)
+ }
+ }.into_string();
yield R::Ok(Bytes::from(error_html));
return;
};