"#,
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])
)
}
fn render_results(response: Response) -> String {
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])
));
}
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])
));
}
if let Some(featured_snippet) = response.featured_snippet {
html.push_str(&render_featured_snippet(&featured_snippet));
}
for result in &response.search_results {
html.push_str(&render_search_result(result));
}
html
}
fn render_engine_progress_update(
engine: Engine,
progress_update: &EngineProgressUpdate,
time_ms: u64,
) -> String {
let message = match progress_update {
EngineProgressUpdate::Requesting => "requesting",
EngineProgressUpdate::Downloading => "downloading",
EngineProgressUpdate::Parsing => "parsing",
EngineProgressUpdate::Done => "done",
};
format!(r#"{time_ms:>4}ms {engine} {message}"#)
}
pub async fn route(
Query(params): Query>,
headers: HeaderMap,
ConnectInfo(addr): ConnectInfo,
) -> impl IntoResponse {
let query = params
.get("q")
.cloned()
.unwrap_or_default()
.trim()
.replace('\n', " ");
if query.is_empty() {
// redirect to index
return (
StatusCode::FOUND,
[
(header::LOCATION, "/"),
(header::CONTENT_TYPE, "text/html; charset=utf-8"),
],
Body::from("No query provided, click here to go back to index"),
);
}
let query = SearchQuery {
query,
request_headers: headers
.clone()
.into_iter()
.map(|(k, v)| {
(
k.map(|k| k.to_string()).unwrap_or_default(),
v.to_str().unwrap_or_default().to_string(),
)
})
.collect(),
ip: headers
// this could be exploited under some setups, but the ip is only used for the
// "what is my ip" answer so it doesn't really matter
.get("x-forwarded-for")
.map(|ip| ip.to_str().unwrap_or_default().to_string())
.unwrap_or_else(|| addr.ip().to_string()),
};
let s = stream! {
type R = Result;
// the html is sent in three chunks (technically more if you count progress updates):
// 1) the beginning of the html, including the search bar
// 1.5) the progress updates
// 2) the results
// 3) the post-search infobox (usually not sent) + the end of the html
let first_part = render_beginning_of_html(&query);
// second part is in the loop
let mut third_part = String::new();
yield R::Ok(Bytes::from(first_part));
let (progress_tx, mut progress_rx) = tokio::sync::mpsc::unbounded_channel();
let search_future = tokio::spawn(async move { engines::search(query, progress_tx).await });
while let Some(progress_update) = progress_rx.recv().await {
match progress_update.data {
ProgressUpdateData::Engine { engine, update } => {
let progress_html = format!(
r#"