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