// This file is part of September .
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
// Copyright (C) 2022-2023 Fuwn
// SPDX-License-Identifier: GPL-3.0-only
use {germ::ast::Node, std::env::var, url::Url};
fn link_from_host_href(url: &Url, href: &str) -> Option {
Some(format!(
"gemini://{}{}{}",
url.domain()?,
{ if href.starts_with('/') { "" } else { "/" } },
href
))
}
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
pub fn from_gemini(
response: &germ::request::Response,
url: &Url,
is_proxy: bool,
) -> Option<(String, String)> {
let ast_tree =
germ::ast::Ast::from_string(response.content().clone().unwrap_or_default());
let ast = ast_tree.inner();
let mut html = String::new();
let mut title = String::new();
let safe = html_escape::encode_text;
let mut previous_link = false;
let condense_links = {
let links = var("CONDENSE_LINKS").map_or_else(
|_| vec![],
|condense_links| {
condense_links
.split(',')
.map(std::string::ToString::to_string)
.collect()
},
);
links.contains(&url.path().to_string()) || links.contains(&"*".to_string())
};
for node in ast {
if previous_link && (!matches!(node, Node::Link { .. }) || !condense_links)
{
html.push_str("\n
");
previous_link = false;
} else if previous_link {
html.push_str(" | ");
} else if !previous_link && matches!(node, Node::Link { .. }) {
html.push_str("");
}
match node {
Node::Text(text) => html.push_str(&format!("
{}
", safe(text))),
Node::Link { to, text } => {
let mut href = to.clone();
let mut surface = false;
if href.contains("://") && !href.starts_with("gemini://") {
surface = true;
} else if !href.starts_with("gemini://") && !href.starts_with('/') {
href = format!("./{href}");
} else if href.starts_with('/') || !href.contains("://") {
href = link_from_host_href(url, &href)?;
}
if var("PROXY_BY_DEFAULT")
.unwrap_or_else(|_| "true".to_string())
.to_lowercase()
== "true"
&& href.contains("gemini://")
&& !surface
{
if is_proxy
|| href
.trim_start_matches("gemini://")
.trim_end_matches('/')
.split('/')
.collect::>()
.first()
.unwrap()
!= &url.host().unwrap().to_string().as_str()
{
href = format!("/proxy/{}", href.trim_start_matches("gemini://"));
} else {
href = href.trim_start_matches("gemini://").replacen(
&if let Some(host) = url.host() {
host.to_string()
} else {
return None;
},
"",
1,
);
}
}
if let Ok(keeps) = var("KEEP_GEMINI_EXACT") {
let mut keeps = keeps.split(',');
if (href.starts_with('/') || !href.contains("://")) && !surface {
let temporary_href = link_from_host_href(url, &href)?;
if keeps.any(|k| k == &*temporary_href) {
href = temporary_href;
}
}
}
if let Ok(keeps) = var("KEEP_GEMINI_DOMAIN") {
let host = if let Some(host) = url.host() {
host.to_string()
} else {
return None;
};
if (href.starts_with('/')
|| !href.contains("://") && keeps.split(',').any(|k| k == &*host))
&& !surface
{
href = link_from_host_href(url, &href)?;
}
}
if let Ok(embed_images) = var("EMBED_IMAGES") {
if let Some(extension) = std::path::Path::new(&href).extension() {
if extension == "png"
|| extension == "jpg"
|| extension == "jpeg"
|| extension == "gif"
|| extension == "webp"
|| extension == "svg"
{
if embed_images == "1" {
html.push_str(&format!(
"{} Embedded below
\n",
href,
safe(&text.clone().unwrap_or_default()),
));
}
html.push_str(&format!(
"
\n",
safe(&href),
safe(&text.clone().unwrap_or_default())
));
continue;
}
}
}
previous_link = true;
html.push_str(&format!(
"{}",
href,
safe(&text.clone().unwrap_or_default()),
));
}
Node::Heading { level, text } => {
if title.is_empty() && *level == 1 {
title = safe(&text.clone()).to_string();
}
html.push_str(&format!(
"<{}>{}{0}>",
match level {
1 => "h1",
2 => "h2",
3 => "h3",
_ => "p",
},
safe(text),
));
}
Node::List(items) => html.push_str(&format!(
"",
items
.iter()
.map(|i| format!("{i}"))
.collect::>()
.join("\n")
)),
Node::Blockquote(text) => {
html.push_str(&format!("{}
", safe(text)));
}
Node::PreformattedText { text, .. } => {
html.push_str(&format!("{}", safe(text)));
}
Node::Whitespace => {}
}
}
Some((title, html))
}