✨ [html] Añade soporte para unidades CSS
This commit is contained in:
parent
ed22d3d591
commit
3c848cb368
7 changed files with 494 additions and 5 deletions
9
Cargo.lock
generated
9
Cargo.lock
generated
|
|
@ -1568,6 +1568,7 @@ dependencies = [
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pastey",
|
"pastey",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"substring",
|
"substring",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"terminal_size",
|
"terminal_size",
|
||||||
|
|
@ -1914,9 +1915,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.11.3"
|
version = "1.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c"
|
checksum = "4a52d8d02cacdb176ef4678de6c052efb4b3da14b78e4db683a4252762be5433"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
|
@ -1926,9 +1927,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.11"
|
version = "0.4.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad"
|
checksum = "722166aa0d7438abbaa4d5cc2c649dac844e8c56d82fb3d33e9c34b5cd268fc6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ testing = []
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.23"
|
tempfile = "3.23"
|
||||||
|
serde_json = "1.0"
|
||||||
pagetop-aliner.workspace = true
|
pagetop-aliner.workspace = true
|
||||||
pagetop-bootsier.workspace = true
|
pagetop-bootsier.workspace = true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,11 @@ use crate::{core, AutoDefault};
|
||||||
#[allow(type_alias_bounds)]
|
#[allow(type_alias_bounds)]
|
||||||
pub type OptionComponent<C: core::component::Component> = core::component::Typed<C>;
|
pub type OptionComponent<C: core::component::Component> = core::component::Typed<C>;
|
||||||
|
|
||||||
|
mod unit;
|
||||||
|
pub use unit::UnitValue;
|
||||||
|
|
||||||
|
// **< HTML PrepareMarkup >*************************************************************************
|
||||||
|
|
||||||
/// Prepara contenido HTML para su conversión a [`Markup`].
|
/// Prepara contenido HTML para su conversión a [`Markup`].
|
||||||
///
|
///
|
||||||
/// Este tipo encapsula distintos orígenes de contenido HTML (texto plano, HTML sin escapar o
|
/// Este tipo encapsula distintos orígenes de contenido HTML (texto plano, HTML sin escapar o
|
||||||
|
|
|
||||||
259
src/html/unit.rs
Normal file
259
src/html/unit.rs
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
use crate::AutoDefault;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Deserializer};
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// Representa una **unidad CSS** lista para formatear o deserializar.
|
||||||
|
///
|
||||||
|
/// ## Unidades soportadas
|
||||||
|
///
|
||||||
|
/// - **Absolutas** *(valores enteros, `isize`)*:
|
||||||
|
/// - `Cm(isize)` - `cm` (centímetros)
|
||||||
|
/// - `In(isize)` - `in` (pulgadas; `1in = 96px = 2.54cm`)
|
||||||
|
/// - `Mm(isize)` - `mm` (milímetros)
|
||||||
|
/// - `Pc(isize)` - `pc` (picas; `1pc = 12pt`)
|
||||||
|
/// - `Pt(isize)` - `pt` (puntos; `1pt = 1/72in`)
|
||||||
|
/// - `Px(isize)` - `px` (píxeles; `1px = 1/96in`)
|
||||||
|
///
|
||||||
|
/// - **Relativas** *(valores decimales, `f32`)*:
|
||||||
|
/// - `RelEm(f32)` - `em` (relativa al tamaño de fuente del elemento)
|
||||||
|
/// - `RelRem(f32)` - `rem` (relativa al tamaño de fuente de `:root`)
|
||||||
|
/// - `RelPct(f32)` - `%` (porcentaje relativo al elemento padre)
|
||||||
|
/// - `RelVh(f32)` - `vh` (1% de la **altura** del viewport)
|
||||||
|
/// - `RelVw(f32)` - `vw` (1% del **ancho** del viewport)
|
||||||
|
///
|
||||||
|
/// ## Valores especiales
|
||||||
|
///
|
||||||
|
/// - `None` - equivale a un texto vacío (`""`), útil para atributos opcionales.
|
||||||
|
/// - `Auto` - equivale a `"auto"`.
|
||||||
|
/// - `Zero` - equivale a `"0"` (cero sin unidad).
|
||||||
|
///
|
||||||
|
/// ## Características
|
||||||
|
///
|
||||||
|
/// - Soporta unidades **absolutas** (`cm`, `in`, `mm`, `pc`, `pt`, `px`) y **relativas** (`em`,
|
||||||
|
/// `rem`, `%`, `vh`, `vw`).
|
||||||
|
/// - `FromStr` para convertir desde texto (p.ej. `"12px"`, `"1.25rem"`, `"auto"`).
|
||||||
|
/// - `Display` para formatear a cadena (p.ej. `UnitValue::Px(12)` genera `"12px"`).
|
||||||
|
/// - `Deserialize` delega en `FromStr`, garantizando una gramática única.
|
||||||
|
///
|
||||||
|
/// ## Ejemplos
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use pagetop::prelude::*;
|
||||||
|
/// use std::str::FromStr;
|
||||||
|
///
|
||||||
|
/// assert_eq!(UnitValue::from_str("16px").unwrap(), UnitValue::Px(16));
|
||||||
|
/// assert_eq!(UnitValue::from_str("1.25rem").unwrap(), UnitValue::RelRem(1.25));
|
||||||
|
/// assert_eq!(UnitValue::from_str("33%").unwrap(), UnitValue::RelPct(33.0));
|
||||||
|
/// assert_eq!(UnitValue::from_str("auto").unwrap(), UnitValue::Auto);
|
||||||
|
/// assert_eq!(UnitValue::from_str("").unwrap(), UnitValue::None);
|
||||||
|
/// assert_eq!(UnitValue::from_str("0").unwrap(), UnitValue::Zero);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## Notas
|
||||||
|
///
|
||||||
|
/// - Las absolutas **no aceptan** decimales (p.ej., `"1.5px"` sería erróneo).
|
||||||
|
/// - Se aceptan signos `+`/`-` en todas las unidades (p.ej., `"-12px"`, `"+0.5em"`).
|
||||||
|
/// - La comparación de unidad es *case-insensitive* al interpretar el texto (`"PX"`, `"Px"`, …).
|
||||||
|
/// - **Sobre píxeles**: Los píxeles (px) son relativos al dispositivo de visualización. En
|
||||||
|
/// dispositivos con baja densidad de píxeles (dpi), 1px equivale a un píxel (punto) del
|
||||||
|
/// dispositivo. En impresoras y pantallas de alta resolución, 1px implica múltiples píxeles del
|
||||||
|
/// dispositivo.
|
||||||
|
/// - **Sobre `em` y `rem`**:
|
||||||
|
/// - `em` es **relativo al tamaño de fuente *del propio elemento***. Si el elemento hereda o
|
||||||
|
/// cambia su `font-size`, todos los valores en `em` dentro de él **se escalan en cascada**.
|
||||||
|
/// - `rem` es **relativo al tamaño de fuente del elemento raíz** (`:root`/`html`), **no se verá
|
||||||
|
/// afectado** por cambios de `font-size` en elementos anidados.
|
||||||
|
/// - Ejemplo: si `:root { font-size: 16px }` y un contenedor tiene `font-size: 20px`, entonces
|
||||||
|
/// dentro del contenedor `1em == 20px` pero `1rem == 16px`.
|
||||||
|
/// - Uso típico: `rem` para tipografía y espaciados globales (consistencia al cambiar la base del
|
||||||
|
/// sitio); `em` para tamaños que deban escalar **con el propio componente** (p.ej.,
|
||||||
|
/// `padding: 0.5em` que crece si el componente aumenta su `font-size`).
|
||||||
|
/// - **Sobre el viewport**: Si el ancho de la ventana del navegador es de 50cm, 1vw equivale a
|
||||||
|
/// 0.5cm (1vw siempre es 1% del ancho del viewport, independientemente del zoom del navegador o
|
||||||
|
/// la densidad de píxeles del dispositivo).
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum UnitValue {
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
Auto,
|
||||||
|
/// Cero sin unidad.
|
||||||
|
Zero,
|
||||||
|
/// Centímetros.
|
||||||
|
Cm(isize),
|
||||||
|
/// Pulgadas (1in = 96px = 2.54cm).
|
||||||
|
In(isize),
|
||||||
|
/// Milímetros.
|
||||||
|
Mm(isize),
|
||||||
|
/// Picas (1pc = 12pt).
|
||||||
|
Pc(isize),
|
||||||
|
/// Puntos (1pt = 1/72in).
|
||||||
|
Pt(isize),
|
||||||
|
/// Píxeles (1px = 1/96in).
|
||||||
|
Px(isize),
|
||||||
|
/// Relativo al tamaño de la fuente del elemento.
|
||||||
|
RelEm(f32),
|
||||||
|
/// Relativo al tamaño de la fuente del elemento raíz.
|
||||||
|
RelRem(f32),
|
||||||
|
/// Porcentaje relativo al elemento padre.
|
||||||
|
RelPct(f32),
|
||||||
|
/// Relativo al 1% de la altura del viewport.
|
||||||
|
RelVh(f32),
|
||||||
|
/// Relativo al 1% del ancho del viewport.
|
||||||
|
RelVw(f32),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formatea la unidad como cadena CSS.
|
||||||
|
///
|
||||||
|
/// Reglas:
|
||||||
|
///
|
||||||
|
/// - `None` - `""` (cadena vacía).
|
||||||
|
/// - `Auto` - `"auto"`.
|
||||||
|
/// - `Zero` - `"0"` (cero sin unidad).
|
||||||
|
/// - Absolutas - entero con su unidad: `Px(12)` a `"12px"`.
|
||||||
|
/// - Relativas - número en punto flotante; si es entero, se imprime sin decimales:
|
||||||
|
/// - `RelEm(2.0)` a `"2em"`
|
||||||
|
/// - `RelPct(33.5)` a `"33.5%"`
|
||||||
|
#[rustfmt::skip]
|
||||||
|
impl fmt::Display for UnitValue {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
UnitValue::None => write!(f, ""),
|
||||||
|
UnitValue::Auto => write!(f, "auto"),
|
||||||
|
UnitValue::Zero => write!(f, "0"),
|
||||||
|
// Valor absoluto.
|
||||||
|
UnitValue::Cm(av) => write!(f, "{av}cm"),
|
||||||
|
UnitValue::In(av) => write!(f, "{av}in"),
|
||||||
|
UnitValue::Mm(av) => write!(f, "{av}mm"),
|
||||||
|
UnitValue::Pc(av) => write!(f, "{av}pc"),
|
||||||
|
UnitValue::Pt(av) => write!(f, "{av}pt"),
|
||||||
|
UnitValue::Px(av) => write!(f, "{av}px"),
|
||||||
|
// Valor relativo.
|
||||||
|
UnitValue::RelEm(rv) => write!(f, "{rv}em"),
|
||||||
|
UnitValue::RelRem(rv) => write!(f, "{rv}rem"),
|
||||||
|
UnitValue::RelPct(rv) => write!(f, "{rv}%"),
|
||||||
|
UnitValue::RelVh(rv) => write!(f, "{rv}vh"),
|
||||||
|
UnitValue::RelVw(rv) => write!(f, "{rv}vw"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convierte una cadena a [`UnitValue`] siguiendo una gramática CSS acotada.
|
||||||
|
///
|
||||||
|
/// ## Acepta
|
||||||
|
///
|
||||||
|
/// - `""` para `UnitValue::None`
|
||||||
|
/// - `"auto"`
|
||||||
|
/// - **Cero sin unidad**: `"0"`, `"+0"`, `"-0"`, `"0.0"`, `"0."`, `".0"` para `UnitValue::Zero`
|
||||||
|
/// - Porcentaje: `"<n>%"` (p.ej. `"33%"`, `"33 %"`)
|
||||||
|
/// - Absolutas enteras: `"<entero><unidad>"`, p.ej. `"12px"`, `"-5pt"`
|
||||||
|
/// - Relativas decimales: `"<float><unidad>"`, p.ej. `"1.25rem"`, `"-0.5vh"`, `".5em"`, `"1.rem"`
|
||||||
|
///
|
||||||
|
/// (Se toleran espacios entre número y unidad: `"12 px"`, `"1.5 rem"`).
|
||||||
|
///
|
||||||
|
/// ## Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use pagetop::prelude::*;
|
||||||
|
/// use std::str::FromStr;
|
||||||
|
///
|
||||||
|
/// assert_eq!(UnitValue::from_str("12px").unwrap(), UnitValue::Px(12));
|
||||||
|
/// assert!(UnitValue::from_str("12").is_err());
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## Errores de interpretación
|
||||||
|
///
|
||||||
|
/// - Falta la unidad cuando es necesaria (p.ej. `"12"`, excepto para el valor cero).
|
||||||
|
/// - Decimales en valores que deben ser absolutos (p.ej. `"1.5px"`).
|
||||||
|
/// - Unidades desconocidas (p.ej. `"10ch"`, no soportada aún).
|
||||||
|
/// - Notación científica o bases no decimales: `"1e3vw"`, `"0x10px"` (no soportadas). Los ceros a
|
||||||
|
/// la izquierda (p. ej. `"020px"`) se interpretan en **base 10** (`20px`).
|
||||||
|
///
|
||||||
|
/// La comparación de la unidad es *case-insensitive*.
|
||||||
|
impl FromStr for UnitValue {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
let s = input.trim();
|
||||||
|
if s.is_empty() {
|
||||||
|
return Ok(UnitValue::None);
|
||||||
|
}
|
||||||
|
if s.eq_ignore_ascii_case("auto") {
|
||||||
|
return Ok(UnitValue::Auto);
|
||||||
|
}
|
||||||
|
|
||||||
|
match s.find(|c: char| c.is_ascii_alphabetic() || c == '%') {
|
||||||
|
None => {
|
||||||
|
let n: f32 = s
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| format!("Invalid number `{s}`: {e}"))?;
|
||||||
|
if n == 0.0 {
|
||||||
|
Ok(UnitValue::Zero)
|
||||||
|
} else {
|
||||||
|
Err(
|
||||||
|
"Missing unit (expected one of cm,in,mm,pc,pt,px,em,rem,vh,vw, or %)"
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(split_pos) => {
|
||||||
|
let (num_str, unit_str) = s.split_at(split_pos);
|
||||||
|
let u = unit_str.trim();
|
||||||
|
let n = num_str.trim();
|
||||||
|
|
||||||
|
let parse_abs = |n_s: &str| -> Result<isize, String> {
|
||||||
|
n_s.parse::<isize>()
|
||||||
|
.map_err(|e| format!("Invalid integer `{n_s}`: {e}"))
|
||||||
|
};
|
||||||
|
let parse_rel = |n_s: &str| -> Result<f32, String> {
|
||||||
|
n_s.parse::<f32>()
|
||||||
|
.map_err(|e| format!("Invalid float `{n_s}`: {e}"))
|
||||||
|
};
|
||||||
|
|
||||||
|
match u.to_ascii_lowercase().as_str() {
|
||||||
|
// Unidades absolutas.
|
||||||
|
"cm" => Ok(UnitValue::Cm(parse_abs(n)?)),
|
||||||
|
"in" => Ok(UnitValue::In(parse_abs(n)?)),
|
||||||
|
"mm" => Ok(UnitValue::Mm(parse_abs(n)?)),
|
||||||
|
"pc" => Ok(UnitValue::Pc(parse_abs(n)?)),
|
||||||
|
"pt" => Ok(UnitValue::Pt(parse_abs(n)?)),
|
||||||
|
"px" => Ok(UnitValue::Px(parse_abs(n)?)),
|
||||||
|
// Unidades relativas.
|
||||||
|
"em" => Ok(UnitValue::RelEm(parse_rel(n)?)),
|
||||||
|
"rem" => Ok(UnitValue::RelRem(parse_rel(n)?)),
|
||||||
|
"vh" => Ok(UnitValue::RelVh(parse_rel(n)?)),
|
||||||
|
"vw" => Ok(UnitValue::RelVw(parse_rel(n)?)),
|
||||||
|
// Porcentaje como unidad.
|
||||||
|
"%" => Ok(UnitValue::RelPct(parse_rel(n)?)),
|
||||||
|
// Unidad desconocida.
|
||||||
|
_ => Err(format!("Unknown unit: `{u}`")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializa desde una cadena usando la misma gramática que [`FromStr`].
|
||||||
|
///
|
||||||
|
/// ### Ejemplo con `serde_json`
|
||||||
|
/// ```rust
|
||||||
|
/// # use pagetop::prelude::*;
|
||||||
|
/// use serde::Deserialize;
|
||||||
|
///
|
||||||
|
/// #[derive(Deserialize)]
|
||||||
|
/// struct Style { width: UnitValue }
|
||||||
|
///
|
||||||
|
/// // "{\"width\":\"12px\"}" deserializa como `Style { width: UnitValue::Px(12) }`
|
||||||
|
/// ```
|
||||||
|
impl<'de> Deserialize<'de> for UnitValue {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let raw = String::deserialize(deserializer)?;
|
||||||
|
raw.parse().map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,7 +34,7 @@ pub use crate::trace;
|
||||||
pub use crate::html::{
|
pub use crate::html::{
|
||||||
display, html_private, Asset, Assets, AttrClasses, AttrId, AttrL10n, AttrName, AttrValue,
|
display, html_private, Asset, Assets, AttrClasses, AttrId, AttrL10n, AttrName, AttrValue,
|
||||||
ClassesOp, Escaper, Favicon, JavaScript, Markup, PreEscaped, PrepareMarkup, StyleSheet,
|
ClassesOp, Escaper, Favicon, JavaScript, Markup, PreEscaped, PrepareMarkup, StyleSheet,
|
||||||
TargetMedia, DOCTYPE,
|
TargetMedia, UnitValue, DOCTYPE,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use crate::locale::*;
|
pub use crate::locale::*;
|
||||||
|
|
|
||||||
223
tests/html_unit.rs
Normal file
223
tests/html_unit.rs
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn unit_value_empty_and_auto_and_zero_without_unit() {
|
||||||
|
assert_eq!(UnitValue::from_str("").unwrap(), UnitValue::None);
|
||||||
|
assert_eq!(UnitValue::from_str("auto").unwrap(), UnitValue::Auto);
|
||||||
|
assert_eq!(UnitValue::from_str("AUTO").unwrap(), UnitValue::Auto);
|
||||||
|
|
||||||
|
// Cero sin unidad.
|
||||||
|
assert_eq!(UnitValue::from_str("0").unwrap(), UnitValue::Zero);
|
||||||
|
assert_eq!(UnitValue::from_str("+0").unwrap(), UnitValue::Zero);
|
||||||
|
assert_eq!(UnitValue::from_str("-0").unwrap(), UnitValue::Zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn unit_value_absolute_integers_with_signs_and_spaces_and_case() {
|
||||||
|
// Positivos, negativos y con espacios.
|
||||||
|
assert_eq!(UnitValue::from_str("12px").unwrap(), UnitValue::Px(12));
|
||||||
|
assert_eq!(UnitValue::from_str("-5pt").unwrap(), UnitValue::Pt(-5));
|
||||||
|
assert_eq!(UnitValue::from_str(" 7 cm ").unwrap(), UnitValue::Cm(7));
|
||||||
|
assert_eq!(UnitValue::from_str("+9 in").unwrap(), UnitValue::In(9));
|
||||||
|
assert_eq!(UnitValue::from_str(" 13 mm ").unwrap(), UnitValue::Mm(13));
|
||||||
|
assert_eq!(UnitValue::from_str("4 pc").unwrap(), UnitValue::Pc(4));
|
||||||
|
|
||||||
|
// Insensibilidad a mayúsculas.
|
||||||
|
assert_eq!(UnitValue::from_str("10PX").unwrap(), UnitValue::Px(10));
|
||||||
|
assert_eq!(UnitValue::from_str("15Pt").unwrap(), UnitValue::Pt(15));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn unit_value_relative_floats_with_signs_and_spaces_and_case() {
|
||||||
|
assert_eq!(
|
||||||
|
UnitValue::from_str("1.25rem").unwrap(),
|
||||||
|
UnitValue::RelRem(1.25)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
UnitValue::from_str("-0.5em").unwrap(),
|
||||||
|
UnitValue::RelEm(-0.5)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
UnitValue::from_str(" 33% ").unwrap(),
|
||||||
|
UnitValue::RelPct(33.0)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
UnitValue::from_str(" -12.5 vh").unwrap(),
|
||||||
|
UnitValue::RelVh(-12.5)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
UnitValue::from_str(" 8.0 VW ").unwrap(),
|
||||||
|
UnitValue::RelVw(8.0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn unit_value_whitespace_between_number_and_unit_is_allowed() {
|
||||||
|
// Hay espacio entre número y unidad (la implementación actual lo admite).
|
||||||
|
assert_eq!(UnitValue::from_str("12 px").unwrap(), UnitValue::Px(12));
|
||||||
|
assert_eq!(
|
||||||
|
UnitValue::from_str("1.5 rem").unwrap(),
|
||||||
|
UnitValue::RelRem(1.5)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
UnitValue::from_str("25 %").unwrap(),
|
||||||
|
UnitValue::RelPct(25.0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn unit_value_roundtrip_display_keeps_expected_format() {
|
||||||
|
let cases = [
|
||||||
|
("", UnitValue::None, ""),
|
||||||
|
("auto", UnitValue::Auto, "auto"),
|
||||||
|
("0", UnitValue::Zero, "0"),
|
||||||
|
("12px", UnitValue::Px(12), "12px"),
|
||||||
|
("-5pt", UnitValue::Pt(-5), "-5pt"),
|
||||||
|
("7cm", UnitValue::Cm(7), "7cm"),
|
||||||
|
("33%", UnitValue::RelPct(33.0), "33%"),
|
||||||
|
("1.25rem", UnitValue::RelRem(1.25), "1.25rem"),
|
||||||
|
("2em", UnitValue::RelEm(2.0), "2em"),
|
||||||
|
("-0.5vh", UnitValue::RelVh(-0.5), "-0.5vh"),
|
||||||
|
("8vw", UnitValue::RelVw(8.0), "8vw"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (input, expected_value, expected_display) in cases {
|
||||||
|
let parsed = UnitValue::from_str(input).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
parsed, expected_value,
|
||||||
|
"parsed mismatch for input `{input}`"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parsed.to_string(),
|
||||||
|
expected_display,
|
||||||
|
"display mismatch for input `{input}`"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn unit_value_percentage_trimming_and_signs() {
|
||||||
|
assert_eq!(
|
||||||
|
UnitValue::from_str(" 12.5 % ").unwrap(),
|
||||||
|
UnitValue::RelPct(12.5)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
UnitValue::from_str("-0.0%").unwrap(),
|
||||||
|
UnitValue::RelPct(-0.0)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
UnitValue::from_str("+15%").unwrap(),
|
||||||
|
UnitValue::RelPct(15.0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ERRORES ESPERADOS (no cambiar los mensajes; con is_err() basta).
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn unit_value_errors_missing_unit_for_non_zero() {
|
||||||
|
assert!(
|
||||||
|
UnitValue::from_str("12").is_err(),
|
||||||
|
"non-zero without unit must error"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
UnitValue::from_str(" -3 ").is_err(),
|
||||||
|
"non-zero without unit must error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn unit_value_errors_decimals_in_absolute_units() {
|
||||||
|
assert!(UnitValue::from_str("1.5px").is_err());
|
||||||
|
assert!(UnitValue::from_str("-2.0pt").is_err());
|
||||||
|
assert!(UnitValue::from_str("+0.1cm").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn unit_value_errors_unknown_units_or_bad_percentages() {
|
||||||
|
// Unidad no soportada.
|
||||||
|
assert!(UnitValue::from_str("10ch").is_err());
|
||||||
|
assert!(UnitValue::from_str("2q").is_err());
|
||||||
|
// Falta número.
|
||||||
|
assert!(UnitValue::from_str("%").is_err());
|
||||||
|
assert!(UnitValue::from_str(" % ").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn unit_value_errors_non_numeric_numbers() {
|
||||||
|
assert!(UnitValue::from_str("NaNem").is_err());
|
||||||
|
// Decimal no permitido por FromStr.
|
||||||
|
assert!(UnitValue::from_str("1,5rem").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn unit_value_serde_deserialize_struct_and_array() {
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, PartialEq)]
|
||||||
|
struct BoxStyle {
|
||||||
|
width: UnitValue,
|
||||||
|
height: UnitValue,
|
||||||
|
margin: UnitValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
let json = r#"{ "width": "12px", "height": "1.5rem", "margin": "0" }"#;
|
||||||
|
let s: BoxStyle = serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(s.width, UnitValue::Px(12));
|
||||||
|
assert_eq!(s.height, UnitValue::RelRem(1.5));
|
||||||
|
assert_eq!(s.margin, UnitValue::Zero);
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, PartialEq)]
|
||||||
|
struct Many {
|
||||||
|
values: Vec<UnitValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let json_arr = r#"{ "values": ["", "auto", "33%", "8vw", "7 cm", "-5pt"] }"#;
|
||||||
|
let m: Many = serde_json::from_str(json_arr).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
m.values,
|
||||||
|
vec![
|
||||||
|
UnitValue::None,
|
||||||
|
UnitValue::Auto,
|
||||||
|
UnitValue::RelPct(33.0),
|
||||||
|
UnitValue::RelVw(8.0),
|
||||||
|
UnitValue::Cm(7),
|
||||||
|
UnitValue::Pt(-5),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn unit_value_accepts_dot5_and_1dot_shorthand_for_relatives() {
|
||||||
|
// `.5` y `1.` se parsean correctamente en relativas.
|
||||||
|
assert_eq!(UnitValue::from_str(".5em").unwrap(), UnitValue::RelEm(0.5));
|
||||||
|
assert_eq!(
|
||||||
|
UnitValue::from_str("1.rem").unwrap(),
|
||||||
|
UnitValue::RelRem(1.0)
|
||||||
|
);
|
||||||
|
assert_eq!(UnitValue::from_str("1.vh").unwrap(), UnitValue::RelVh(1.0));
|
||||||
|
// Sin unidad debe seguir fallando.
|
||||||
|
assert!(UnitValue::from_str("1.").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn unit_value_display_keeps_minus_zero_for_relatives() {
|
||||||
|
// Comportamiento actual: f32 Display muestra "-0" si el valor es -0.0.
|
||||||
|
let v = UnitValue::RelEm(-0.0);
|
||||||
|
// Se acepta cualquiera de los dos formatos como válidos.
|
||||||
|
let s = v.to_string();
|
||||||
|
assert!(
|
||||||
|
s == "-0em" || s == "0em",
|
||||||
|
"current Display prints `{s}` for -0.0; both are acceptable in tests"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn unit_value_rejects_non_decimal_notations() {
|
||||||
|
// Octal, los ceros a la izquierda (p.ej. `"020px"`) se interpretan en **base 10** (`20px`).
|
||||||
|
assert_eq!(UnitValue::from_str("020px").unwrap(), UnitValue::Px(20));
|
||||||
|
// Notación científica y bases no decimales (p.ej. `"1e3vw"`, `"0x10px"`) no están soportadas.
|
||||||
|
assert!(UnitValue::from_str("1e3vw").is_err());
|
||||||
|
assert!(UnitValue::from_str("0x10px").is_err());
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue