use std::collections::HashMap;
use std::str::FromStr;
use euclid::Scale;
use serde::{Deserialize, Serialize};
use servo_geometry::DeviceIndependentPixel;
use style_traits::CSSPixel;
pub const MIN_PAGE_ZOOM: Scale<f32, CSSPixel, DeviceIndependentPixel> = Scale::new(0.1);
pub const MAX_PAGE_ZOOM: Scale<f32, CSSPixel, DeviceIndependentPixel> = Scale::new(10.0);
pub const DEFAULT_PAGE_ZOOM: Scale<f32, CSSPixel, DeviceIndependentPixel> = Scale::new(1.0);
const SEPARATORS: [char; 2] = [',', ';'];
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ViewportDescription {
pub initial_scale: Scale<f32, CSSPixel, DeviceIndependentPixel>,
pub minimum_scale: Scale<f32, CSSPixel, DeviceIndependentPixel>,
pub maximum_scale: Scale<f32, CSSPixel, DeviceIndependentPixel>,
pub user_scalable: UserScalable,
}
#[derive(Debug)]
pub enum ViewportDescriptionParseError {
Empty,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub enum UserScalable {
No = 0,
Yes = 1,
}
impl TryFrom<&str> for UserScalable {
type Error = &'static str;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value.to_lowercase().as_str() {
"yes" => Ok(UserScalable::Yes),
"no" => Ok(UserScalable::No),
_ => match value.parse::<f32>() {
Ok(1.0) => Ok(UserScalable::Yes),
Ok(0.0) => Ok(UserScalable::No),
_ => Err("can't convert character to UserScalable"),
},
}
}
}
impl Default for ViewportDescription {
fn default() -> Self {
ViewportDescription {
initial_scale: DEFAULT_PAGE_ZOOM,
minimum_scale: MIN_PAGE_ZOOM,
maximum_scale: MAX_PAGE_ZOOM,
user_scalable: UserScalable::Yes,
}
}
}
impl ViewportDescription {
fn process_viewport_key_value_pairs(pairs: HashMap<String, String>) -> ViewportDescription {
let mut description = ViewportDescription::default();
for (key, value) in &pairs {
match key.as_str() {
"initial-scale" => {
if let Some(zoom) = Self::parse_viewport_value_as_zoom(value) {
description.initial_scale = zoom;
}
},
"minimum-scale" => {
if let Some(zoom) = Self::parse_viewport_value_as_zoom(value) {
description.minimum_scale = zoom;
}
},
"maximum-scale" => {
if let Some(zoom) = Self::parse_viewport_value_as_zoom(value) {
description.maximum_scale = zoom;
}
},
"user-scalable" => {
if let Ok(user_zoom_allowed) = value.as_str().try_into() {
description.user_scalable = user_zoom_allowed;
}
},
_ => (),
}
}
description.initial_scale =
Scale::new(description.clamp_zoom(description.initial_scale.get()));
description
}
fn parse_viewport_value_as_zoom(
value: &str,
) -> Option<Scale<f32, CSSPixel, DeviceIndependentPixel>> {
value
.to_lowercase()
.as_str()
.parse::<f32>()
.ok()
.filter(|&n| (0.0..=10.0).contains(&n))
.map(Scale::new)
}
pub fn clamp_zoom(&self, zoom: f32) -> f32 {
zoom.clamp(self.minimum_scale.get(), self.maximum_scale.get())
}
}
impl FromStr for ViewportDescription {
type Err = ViewportDescriptionParseError;
fn from_str(string: &str) -> Result<Self, Self::Err> {
if string.is_empty() {
return Err(ViewportDescriptionParseError::Empty);
}
let parsed_values = string
.split(SEPARATORS)
.filter_map(|pair| {
let mut parts = pair.split('=').map(str::trim);
if let (Some(key), Some(value)) = (parts.next(), parts.next()) {
Some((key.to_string(), value.to_string()))
} else {
None
}
})
.collect::<HashMap<String, String>>();
Ok(Self::process_viewport_key_value_pairs(parsed_values))
}
}