use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use crate::Result;
use crate::reqs::ReqDefinition;
pub struct HeadInjection {
pub key: String,
pub html: String,
}
pub struct CodeBlockOutput {
pub html: String,
pub head_injections: Vec<HeadInjection>,
}
impl From<String> for CodeBlockOutput {
fn from(html: String) -> Self {
Self {
html,
head_injections: vec![],
}
}
}
pub trait CodeBlockHandler: Send + Sync {
fn render<'a>(
&'a self,
language: &'a str,
code: &'a str,
) -> Pin<Box<dyn Future<Output = Result<CodeBlockOutput>> + Send + 'a>>;
}
pub type BoxedHandler = Arc<dyn CodeBlockHandler>;
pub trait ReqHandler: Send + Sync {
fn start<'a>(
&'a self,
req: &'a ReqDefinition,
) -> Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>>;
fn end<'a>(
&'a self,
req: &'a ReqDefinition,
) -> Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>>;
}
pub type BoxedReqHandler = Arc<dyn ReqHandler>;
pub trait InlineCodeHandler: Send + Sync {
fn render(&self, code: &str) -> Option<String>;
}
pub type BoxedInlineCodeHandler = Arc<dyn InlineCodeHandler>;
pub trait LinkResolver: Send + Sync {
fn resolve<'a>(
&'a self,
link: &'a str,
source_path: Option<&'a str>,
) -> Pin<Box<dyn Future<Output = Option<String>> + Send + 'a>>;
}
pub type BoxedLinkResolver = Arc<dyn LinkResolver>;
pub struct DefaultReqHandler;
impl ReqHandler for DefaultReqHandler {
fn start<'a>(
&'a self,
req: &'a ReqDefinition,
) -> Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>> {
Box::pin(async move {
Ok(format!(
"<div class=\"req\" id=\"{}\"><a class=\"req-link\" href=\"#{}\" title=\"{}\"><span>{}</span></a>",
req.anchor_id, req.anchor_id, req.id, req.id
))
})
}
fn end<'a>(
&'a self,
_req: &'a ReqDefinition,
) -> Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>> {
Box::pin(async move { Ok("</div>".to_string()) })
}
}
pub struct RawCodeHandler;
impl CodeBlockHandler for RawCodeHandler {
fn render<'a>(
&'a self,
language: &'a str,
code: &'a str,
) -> Pin<Box<dyn Future<Output = Result<CodeBlockOutput>> + Send + 'a>> {
Box::pin(async move {
let escaped = html_escape(code);
let lang_class = if language.is_empty() {
String::new()
} else {
format!(" class=\"language-{}\"", html_escape(language))
};
Ok(format!(
"<div class=\"code-block\"><pre><code{}>{}</code></pre></div>",
lang_class, escaped
)
.into())
})
}
}
pub(crate) fn html_escape(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => result.push_str("&"),
'<' => result.push_str("<"),
'>' => result.push_str(">"),
'"' => result.push_str("""),
'\'' => result.push_str("'"),
_ => result.push(c),
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_html_escape() {
assert_eq!(html_escape("hello"), "hello");
assert_eq!(html_escape("<script>"), "<script>");
assert_eq!(html_escape("a & b"), "a & b");
assert_eq!(html_escape("\"quoted\""), ""quoted"");
}
#[tokio::test]
async fn test_raw_code_handler() {
let handler = RawCodeHandler;
let output = handler.render("rust", "fn main() {}").await.unwrap();
assert_eq!(
output.html,
"<div class=\"code-block\"><pre><code class=\"language-rust\">fn main() {}</code></pre></div>"
);
assert!(output.head_injections.is_empty());
}
#[tokio::test]
async fn test_raw_code_handler_escapes_html() {
let handler = RawCodeHandler;
let output = handler.render("html", "<div>test</div>").await.unwrap();
assert!(output.html.contains("<div>"));
assert!(output.head_injections.is_empty());
}
}