slash-files-rs 0.1.0

Configurable Rust file browser with HTMX UI, JSON API, previews, batch operations, and multi-framework adapters.
Documentation
use std::sync::Arc;

const TEXT_MIME_PREFIX: &str = "text/";
const TEXT_EXTENSIONS: &[&str] = &[
    "c", "cfg", "conf", "cpp", "css", "csv", "env", "go", "h", "hpp", "html", "ini", "java", "js",
    "json", "log", "md", "py", "rs", "sh", "sql", "toml", "ts", "tsx", "txt", "xml", "yaml", "yml",
];

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PreviewRequest {
    pub relative_path: String,
    pub file_name: String,
    pub extension: Option<String>,
    pub mime_type: String,
    pub raw_url: String,
    pub text_excerpt: Option<String>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PreviewDocument {
    pub eyebrow: String,
    pub title: String,
    pub subtitle: String,
    pub body_html: String,
}

impl PreviewDocument {
    pub fn unsupported(request: &PreviewRequest) -> Self {
        Self {
            eyebrow: "Preview".to_string(),
            title: request.file_name.clone(),
            subtitle: request.mime_type.clone(),
            body_html: format!(
                "<div class=\"preview-empty\"><p>This file type does not have a built-in inline renderer yet.</p></div>"
            ),
        }
    }
}

pub trait PreviewHandler: Send + Sync {
    fn render(&self, request: &PreviewRequest) -> Option<PreviewDocument>;
}

#[derive(Clone, Default)]
pub struct PreviewRegistry {
    handlers: Vec<Arc<dyn PreviewHandler>>,
}

impl PreviewRegistry {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn with_handler(mut self, handler: impl PreviewHandler + 'static) -> Self {
        self.handlers.push(Arc::new(handler));
        self
    }

    pub fn render(&self, request: &PreviewRequest) -> PreviewDocument {
        for handler in &self.handlers {
            if let Some(document) = handler.render(request) {
                return document;
            }
        }

        built_in_preview(request).unwrap_or_else(|| PreviewDocument::unsupported(request))
    }
}

fn built_in_preview(request: &PreviewRequest) -> Option<PreviewDocument> {
    let escaped_name = html_escape(&request.file_name);

    if is_text_previewable(request) {
        return Some(text_preview(request));
    }

    if request.mime_type.starts_with("image/") {
        return Some(media_preview(
            request,
            "Image preview",
            format!(
                "<img class=\"preview-media\" src=\"{}\" alt=\"{}\">",
                request.raw_url, escaped_name
            ),
        ));
    }

    if request.mime_type == "application/pdf" {
        return Some(media_preview(
            request,
            "PDF preview",
            format!(
                "<iframe class=\"preview-frame\" src=\"{}\" title=\"{}\"></iframe>",
                request.raw_url, escaped_name
            ),
        ));
    }

    if request.mime_type.starts_with("audio/") {
        return Some(media_preview(
            request,
            "Audio preview",
            format!(
                "<audio class=\"preview-audio\" controls preload=\"metadata\" src=\"{}\"></audio>",
                request.raw_url
            ),
        ));
    }

    if request.mime_type.starts_with("video/") {
        return Some(media_preview(
            request,
            "Video preview",
            format!(
                "<video class=\"preview-video\" controls preload=\"metadata\" src=\"{}\"></video>",
                request.raw_url
            ),
        ));
    }

    None
}

fn is_text_previewable(request: &PreviewRequest) -> bool {
    request.mime_type.starts_with(TEXT_MIME_PREFIX)
        || matches!(
            request.mime_type.as_str(),
            "application/json"
                | "application/javascript"
                | "application/toml"
                | "application/xml"
                | "application/x-yaml"
        )
        || request
            .extension
            .as_deref()
            .map(|extension| TEXT_EXTENSIONS.contains(&extension))
            .unwrap_or(false)
}

fn text_preview(request: &PreviewRequest) -> PreviewDocument {
    let excerpt = request
        .text_excerpt
        .as_deref()
        .map(html_escape)
        .unwrap_or_else(|| "No text content available.".to_string());

    PreviewDocument {
        eyebrow: "Preview".to_string(),
        title: request.file_name.clone(),
        subtitle: request.mime_type.clone(),
        body_html: format!("<pre class=\"preview-code\">{}</pre>", excerpt),
    }
}

fn media_preview(request: &PreviewRequest, eyebrow: &str, body_html: String) -> PreviewDocument {
    PreviewDocument {
        eyebrow: eyebrow.to_string(),
        title: request.file_name.clone(),
        subtitle: request.mime_type.clone(),
        body_html,
    }
}

fn html_escape(input: &str) -> String {
    input
        .replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
        .replace('\'', "&#39;")
}

#[cfg(test)]
mod tests {
    use super::{PreviewDocument, PreviewHandler, PreviewRegistry, PreviewRequest};

    struct CustomHandler;

    impl PreviewHandler for CustomHandler {
        fn render(&self, request: &PreviewRequest) -> Option<PreviewDocument> {
            if request.extension.as_deref() == Some("special") {
                Some(PreviewDocument {
                    eyebrow: "Custom".to_string(),
                    title: request.file_name.clone(),
                    subtitle: "Handled by custom extension".to_string(),
                    body_html: "<div>Custom preview</div>".to_string(),
                })
            } else {
                None
            }
        }
    }

    fn request(extension: Option<&str>, mime_type: &str) -> PreviewRequest {
        PreviewRequest {
            relative_path: "demo/file".to_string(),
            file_name: "file".to_string(),
            extension: extension.map(str::to_string),
            mime_type: mime_type.to_string(),
            raw_url: "/raw".to_string(),
            text_excerpt: Some("hello".to_string()),
        }
    }

    #[test]
    fn custom_handlers_override_builtins() {
        let registry = PreviewRegistry::new().with_handler(CustomHandler);
        let preview = registry.render(&request(Some("special"), "application/octet-stream"));

        assert_eq!(preview.eyebrow, "Custom");
    }

    #[test]
    fn built_in_text_preview_escapes_content() {
        let registry = PreviewRegistry::new();
        let mut request = request(Some("rs"), "text/plain");
        request.text_excerpt = Some("<hello>".to_string());

        let preview = registry.render(&request);

        assert!(preview.body_html.contains("&lt;hello&gt;"));
    }
}