pagetop 0.5.0

Un entorno de desarrollo para crear soluciones web modulares, extensibles y configurables.
Documentation
use crate::core::component::Context;
use crate::html::assets::Asset;
use crate::html::{html, Markup, PreEscaped};
use crate::{util, AutoDefault, CowStr, Weight};

/// Define el origen del recurso CSS y cómo se incluye en el documento.
///
/// Los estilos pueden cargarse desde un archivo externo o estar embebidos directamente en una
/// etiqueta `<style>`.
///
/// - [`From`] - Carga la hoja de estilos desde un archivo externo, insertándola mediante una
///   etiqueta `<link>` con `rel="stylesheet"`.
/// - [`Inline`] - Inserta directamente el contenido CSS dentro de una etiqueta `<style>`.
#[derive(AutoDefault)]
enum Source {
    #[default]
    From(CowStr),
    /// `name`, `closure(&mut Context) -> String`.
    Inline(CowStr, Box<dyn Fn(&mut Context) -> String + Send + Sync>),
}

/// Define el medio objetivo para la hoja de estilos.
///
/// Permite especificar en qué contexto se aplica el CSS, adaptándose a diferentes dispositivos o
/// situaciones de impresión.
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum TargetMedia {
    /// Se aplica en todos los casos (el atributo `media` se omite).
    #[default]
    Default,
    /// Se aplica cuando el documento se imprime.
    Print,
    /// Se aplica en pantallas.
    Screen,
    /// Se aplica en dispositivos que convierten el texto a voz.
    Speech,
}

/// Devuelve el valor para el atributo `media` (`Some(...)`) o `None` para `Default`.
impl TargetMedia {
    const fn as_str(self) -> Option<&'static str> {
        match self {
            TargetMedia::Default => None,
            TargetMedia::Print => Some("print"),
            TargetMedia::Screen => Some("screen"),
            TargetMedia::Speech => Some("speech"),
        }
    }
}

/// Define un recurso **StyleSheet** para incluir en un documento HTML.
///
/// Este tipo permite incluir hojas de estilo CSS externas o embebidas, con soporte para medios
/// específicos (`screen`, `print`, etc.) y [pesos](crate::Weight) que determinan el orden de
/// inserción en el documento.
///
/// > **Nota**
/// > Las hojas de estilo CSS deben estar disponibles en el servidor web de la aplicación. Pueden
/// > servirse usando [`static_files_service!`](crate::static_files_service).
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// // Crea una hoja de estilos externa con control de versión y medio específico (`screen`).
/// let stylesheet = StyleSheet::from("/assets/css/main.css")
///     .with_version("2.0.1")
///     .for_media(TargetMedia::Screen)
///     .with_weight(-10);
///
/// // Crea una hoja de estilos embebida en el documento HTML.
/// let embedded = StyleSheet::inline("custom_theme", |_| r#"
///     body {
///         background-color: #f5f5f5;
///         font-family: 'Segoe UI', sans-serif;
///     }
/// "#.to_string());
/// ```
#[derive(AutoDefault)]
pub struct StyleSheet {
    source: Source,     // Fuente y modo de inclusión del CSS.
    version: CowStr,    // Versión del recurso para la caché del navegador.
    media: TargetMedia, // Medio objetivo para los estilos (`print`, `screen`, ...).
    weight: Weight,     // Peso que determina el orden.
}

impl StyleSheet {
    /// Crea una hoja de estilos externa.
    ///
    /// Equivale a `<link rel="stylesheet" href="...">`.
    pub fn from(path: impl Into<CowStr>) -> Self {
        Self {
            source: Source::From(path.into()),
            ..Default::default()
        }
    }

    /// Crea una hoja de estilos embebida directamente en el documento HTML.
    ///
    /// Equivale a `<style>...</style>`. El parámetro `name` se usa como identificador interno del
    /// recurso.
    ///
    /// La función *closure* recibirá el [`Context`] por si se necesita durante el renderizado.
    pub fn inline<F>(name: impl Into<CowStr>, f: F) -> Self
    where
        F: Fn(&mut Context) -> String + Send + Sync + 'static,
    {
        Self {
            source: Source::Inline(name.into(), Box::new(f)),
            ..Default::default()
        }
    }

    // **< StyleSheet BUILDER >*********************************************************************

    /// Asocia una versión al recurso (usada para control de la caché del navegador).
    ///
    /// Si `version` está vacío, no se añade ningún parámetro a la URL.
    pub fn with_version(mut self, version: impl Into<CowStr>) -> Self {
        self.version = version.into();
        self
    }

    /// Modifica el peso del recurso.
    ///
    /// Los recursos se renderizan de menor a mayor peso. Por defecto es `0`, que respeta el orden
    /// de creación.
    pub fn with_weight(mut self, value: Weight) -> Self {
        self.weight = value;
        self
    }

    // **< StyleSheet HELPERS >*********************************************************************

    /// Especifica el medio donde se aplican los estilos.
    ///
    /// Según el argumento `media`:
    ///
    /// - `TargetMedia::Default` - Se aplica en todos los casos (medio por defecto).
    /// - `TargetMedia::Print`   - Se aplica cuando el documento se imprime.
    /// - `TargetMedia::Screen`  - Se aplica en pantallas.
    /// - `TargetMedia::Speech`  - Se aplica en dispositivos que convierten el texto a voz.
    pub fn for_media(mut self, media: TargetMedia) -> Self {
        self.media = media;
        self
    }
}

impl Asset for StyleSheet {
    /// Devuelve el nombre del recurso, utilizado como clave única.
    ///
    /// Para hojas de estilos externas es la ruta del recurso; para las embebidas, un identificador.
    fn name(&self) -> &str {
        match &self.source {
            Source::From(path) => path,
            Source::Inline(name, _) => name,
        }
    }

    fn weight(&self) -> Weight {
        self.weight
    }

    // **< StyleSheet RENDER >**********************************************************************

    fn render(&self, cx: &mut Context) -> Markup {
        match &self.source {
            Source::From(path) => html! {
                link
                    rel="stylesheet"
                    href=(util::join_pair!(path, "?v=", &self.version))
                    media=[self.media.as_str()];
            },
            Source::Inline(_, f) => html! {
                style { (PreEscaped((f)(cx))) };
            },
        }
    }
}